feat(releases): show indexer name instead of identifier (#1706)

* feat(releases): show indexer name instead of identifier

* feat(releases): remove log in Cell

* feat(releases): update Dashboard recent releases

* fix(releases): db tests

* fix(releases): remove unused code

* fix(releases): remove more unused code

* fix(releases): remove even more unused code

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
This commit is contained in:
ze0s 2024-09-03 14:57:48 +02:00 committed by GitHub
parent 54eab05f1f
commit fd90020400
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 76 additions and 200 deletions

View file

@ -97,19 +97,19 @@ func (repo *ReleaseRepo) StoreReleaseActionStatus(ctx context.Context, status *d
return nil return nil
} }
func (repo *ReleaseRepo) Find(ctx context.Context, params domain.ReleaseQueryParams) ([]*domain.Release, int64, int64, error) { func (repo *ReleaseRepo) Find(ctx context.Context, params domain.ReleaseQueryParams) (*domain.FindReleasesResponse, error) {
tx, err := repo.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) tx, err := repo.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil { if err != nil {
return nil, 0, 0, errors.Wrap(err, "error begin transaction") return nil, errors.Wrap(err, "error begin transaction")
} }
defer tx.Rollback() defer tx.Rollback()
releases, nextCursor, total, err := repo.findReleases(ctx, tx, params) resp, err := repo.findReleases(ctx, tx, params)
if err != nil { if err != nil {
return nil, nextCursor, total, err return nil, err
} }
return releases, nextCursor, total, nil return resp, nil
} }
var reservedSearch = map[string]*regexp.Regexp{ var reservedSearch = map[string]*regexp.Regexp{
@ -126,7 +126,7 @@ var reservedSearch = map[string]*regexp.Regexp{
"r.filter": regexp.MustCompile(`(?i)(?:` + `filter` + `:)(?P<value>'.*?'|".*?"|\S+)`), "r.filter": regexp.MustCompile(`(?i)(?:` + `filter` + `:)(?P<value>'.*?'|".*?"|\S+)`),
} }
func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain.ReleaseQueryParams) ([]*domain.Release, int64, int64, error) { func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain.ReleaseQueryParams) (*domain.FindReleasesResponse, error) {
whereQueryBuilder := sq.And{} whereQueryBuilder := sq.And{}
if params.Cursor > 0 { if params.Cursor > 0 {
whereQueryBuilder = append(whereQueryBuilder, sq.Lt{"r.id": params.Cursor}) whereQueryBuilder = append(whereQueryBuilder, sq.Lt{"r.id": params.Cursor})
@ -172,7 +172,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
whereQuery, _, err := whereQueryBuilder.ToSql() whereQuery, _, err := whereQueryBuilder.ToSql()
if err != nil { if err != nil {
return nil, 0, 0, errors.Wrap(err, "error building wherequery") return nil, errors.Wrap(err, "error building wherequery")
} }
subQueryBuilder := repo.db.squirrel. subQueryBuilder := repo.db.squirrel.
@ -206,53 +206,57 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
subQuery, subArgs, err := subQueryBuilder.ToSql() subQuery, subArgs, err := subQueryBuilder.ToSql()
if err != nil { if err != nil {
return nil, 0, 0, errors.Wrap(err, "error building subquery") return nil, errors.Wrap(err, "error building subquery")
} }
queryBuilder := repo.db.squirrel. queryBuilder := repo.db.squirrel.
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.category", "r.season", "r.episode", "r.year", "r.resolution", "r.source", "r.codec", "r.container", "r.release_group", "r.timestamp", Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "i.id", "i.name", "i.identifier_external", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.category", "r.season", "r.episode", "r.year", "r.resolution", "r.source", "r.codec", "r.container", "r.release_group", "r.timestamp",
"ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp"). "ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp").
Column(sq.Alias(countQuery, "page_total")). Column(sq.Alias(countQuery, "page_total")).
From("release r"). From("release r").
OrderBy("r.id DESC"). OrderBy("r.id DESC").
Where("r.id IN ("+subQuery+")", subArgs...). Where("r.id IN ("+subQuery+")", subArgs...).
LeftJoin("release_action_status ras ON r.id = ras.release_id") LeftJoin("release_action_status ras ON r.id = ras.release_id").
LeftJoin("indexer i ON r.indexer = i.identifier")
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
if err != nil { if err != nil {
return nil, 0, 0, errors.Wrap(err, "error building query") return nil, errors.Wrap(err, "error building query")
} }
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args) repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
res := make([]*domain.Release, 0) resp := &domain.FindReleasesResponse{
Data: make([]*domain.Release, 0),
TotalCount: 0,
NextCursor: 0,
}
rows, err := tx.QueryContext(ctx, query, args...) rows, err := tx.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, 0, 0, errors.Wrap(err, "error executing query") return resp, errors.Wrap(err, "error executing query")
} }
defer rows.Close() defer rows.Close()
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return res, 0, 0, errors.Wrap(err, "error rows findreleases") return resp, errors.Wrap(err, "error rows findreleases")
} }
var countItems int64 = 0
for rows.Next() { for rows.Next() {
var rls domain.Release var rls domain.Release
var ras domain.ReleaseActionStatus var ras domain.ReleaseActionStatus
var rlsindexer, rlsfilter, infoUrl, downloadUrl, codec sql.NullString var rlsIndexer, rlsIndexerName, rlsIndexerExternalName, rlsFilter, infoUrl, downloadUrl, codec sql.NullString
var rlsIndexerID sql.NullInt64
var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64 var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
var rasRejections []sql.NullString var rasRejections []sql.NullString
var rasTimestamp sql.NullTime var rasTimestamp sql.NullTime
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Category, &rls.Season, &rls.Episode, &rls.Year, &rls.Resolution, &rls.Source, &codec, &rls.Container, &rls.Group, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil { if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsIndexer, &rlsIndexerID, &rlsIndexerName, &rlsIndexerExternalName, &rlsFilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Category, &rls.Season, &rls.Episode, &rls.Year, &rls.Resolution, &rls.Source, &codec, &rls.Container, &rls.Group, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &resp.TotalCount); err != nil {
return res, 0, 0, errors.Wrap(err, "error scanning row") return resp, errors.Wrap(err, "error scanning row")
} }
//for _, codec := range codecs { //for _, codec := range codecs {
@ -277,21 +281,25 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
} }
idx := 0 idx := 0
for ; idx < len(res); idx++ { for ; idx < len(resp.Data); idx++ {
if res[idx].ID != rls.ID { if resp.Data[idx].ID != rls.ID {
continue continue
} }
res[idx].ActionStatus = append(res[idx].ActionStatus, ras) resp.Data[idx].ActionStatus = append(resp.Data[idx].ActionStatus, ras)
break break
} }
if idx != len(res) { if idx != len(resp.Data) {
continue continue
} }
rls.Indexer.Identifier = rlsindexer.String rls.Indexer.Identifier = rlsIndexer.String
rls.FilterName = rlsfilter.String rls.Indexer.ID = int(rlsIndexerID.Int64)
rls.Indexer.Name = rlsIndexerName.String
rls.Indexer.IdentifierExternal = rlsIndexerExternalName.String
rls.FilterName = rlsFilter.String
rls.ActionStatus = make([]domain.ReleaseActionStatus, 0) rls.ActionStatus = make([]domain.ReleaseActionStatus, 0)
rls.InfoURL = infoUrl.String rls.InfoURL = infoUrl.String
rls.DownloadURL = downloadUrl.String rls.DownloadURL = downloadUrl.String
@ -302,31 +310,15 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
rls.ActionStatus = append(rls.ActionStatus, ras) rls.ActionStatus = append(rls.ActionStatus, ras)
} }
res = append(res, &rls) resp.Data = append(resp.Data, &rls)
} }
nextCursor := int64(0) if len(resp.Data) > 0 {
if len(res) > 0 { lastID := resp.Data[len(resp.Data)-1].ID
lastID := res[len(res)-1].ID resp.NextCursor = lastID
nextCursor = lastID
} }
return res, nextCursor, countItems, nil return resp, nil
}
func (repo *ReleaseRepo) FindRecent(ctx context.Context) ([]*domain.Release, error) {
tx, err := repo.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return nil, errors.Wrap(err, "error begin transaction")
}
defer tx.Rollback()
releases, _, _, err := repo.findReleases(ctx, tx, domain.ReleaseQueryParams{Limit: 10})
if err != nil {
return nil, err
}
return releases, nil
} }
func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error) { func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error) {

View file

@ -231,12 +231,12 @@ func TestReleaseRepo_Find(t *testing.T) {
Search: "", Search: "",
} }
releases, nextCursor, total, err := repo.Find(context.Background(), queryParams) resp, err := repo.Find(context.Background(), queryParams)
// Verify // Verify
assert.NotNil(t, releases) assert.NotNil(t, resp)
assert.NotEqual(t, int64(0), total) assert.NotEqual(t, int64(0), resp.TotalCount)
assert.True(t, nextCursor >= 0) assert.True(t, resp.NextCursor >= 0)
// Cleanup // Cleanup
_ = repo.Delete(context.Background(), &domain.DeleteReleaseRequest{OlderThan: 0}) _ = repo.Delete(context.Background(), &domain.DeleteReleaseRequest{OlderThan: 0})
@ -281,11 +281,11 @@ func TestReleaseRepo_FindRecent(t *testing.T) {
err = repo.Store(context.Background(), mockData) err = repo.Store(context.Background(), mockData)
assert.NoError(t, err) assert.NoError(t, err)
releases, err := repo.FindRecent(context.Background()) resp, err := repo.Find(context.Background(), domain.ReleaseQueryParams{Limit: 10})
// Verify // Verify
assert.NotNil(t, releases) assert.NotNil(t, resp.Data)
assert.Lenf(t, releases, 1, "Expected 1 release, got %d", len(releases)) assert.Lenf(t, resp.Data, 1, "Expected 1 release, got %d", len(resp.Data))
// Cleanup // Cleanup
_ = repo.Delete(context.Background(), &domain.DeleteReleaseRequest{OlderThan: 0}) _ = repo.Delete(context.Background(), &domain.DeleteReleaseRequest{OlderThan: 0})

View file

@ -31,8 +31,7 @@ import (
type ReleaseRepo interface { type ReleaseRepo interface {
Store(ctx context.Context, release *Release) error Store(ctx context.Context, release *Release) error
Find(ctx context.Context, params ReleaseQueryParams) (res []*Release, nextCursor int64, count int64, err error) Find(ctx context.Context, params ReleaseQueryParams) (*FindReleasesResponse, error)
FindRecent(ctx context.Context) ([]*Release, error)
Get(ctx context.Context, req *GetReleaseRequest) (*Release, error) Get(ctx context.Context, req *GetReleaseRequest) (*Release, error)
GetIndexerOptions(ctx context.Context) ([]string, error) GetIndexerOptions(ctx context.Context) ([]string, error)
Stats(ctx context.Context) (*ReleaseStats, error) Stats(ctx context.Context) (*ReleaseStats, error)
@ -271,6 +270,12 @@ type ReleaseQueryParams struct {
Search string Search string
} }
type FindReleasesResponse struct {
Data []*Release `json:"data"`
TotalCount uint64 `json:"count"`
NextCursor int64 `json:"next_cursor"`
}
type ReleaseActionRetryReq struct { type ReleaseActionRetryReq struct {
ReleaseId int ReleaseId int
ActionStatusId int ActionStatusId int

View file

@ -18,8 +18,7 @@ import (
) )
type releaseService interface { type releaseService interface {
Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error) Find(ctx context.Context, query domain.ReleaseQueryParams) (*domain.FindReleasesResponse, error)
FindRecent(ctx context.Context) (res []*domain.Release, err error)
Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error)
GetIndexerOptions(ctx context.Context) ([]string, error) GetIndexerOptions(ctx context.Context) ([]string, error)
Stats(ctx context.Context) (*domain.ReleaseStats, error) Stats(ctx context.Context) (*domain.ReleaseStats, error)
@ -128,7 +127,7 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
Search: search, Search: search,
} }
releases, nextCursor, count, err := h.service.Find(r.Context(), query) resp, err := h.service.Find(r.Context(), query)
if err != nil { if err != nil {
h.encoder.StatusResponse(w, http.StatusInternalServerError, map[string]any{ h.encoder.StatusResponse(w, http.StatusInternalServerError, map[string]any{
"code": "INTERNAL_SERVER_ERROR", "code": "INTERNAL_SERVER_ERROR",
@ -137,21 +136,11 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
return return
} }
ret := struct { h.encoder.StatusResponse(w, http.StatusOK, resp)
Data []*domain.Release `json:"data"`
NextCursor int64 `json:"next_cursor"`
Count int64 `json:"count"`
}{
Data: releases,
NextCursor: nextCursor,
Count: count,
}
h.encoder.StatusResponse(w, http.StatusOK, ret)
} }
func (h releaseHandler) findRecentReleases(w http.ResponseWriter, r *http.Request) { func (h releaseHandler) findRecentReleases(w http.ResponseWriter, r *http.Request) {
releases, err := h.service.FindRecent(r.Context()) resp, err := h.service.Find(r.Context(), domain.ReleaseQueryParams{Limit: 10})
if err != nil { if err != nil {
h.encoder.StatusResponse(w, http.StatusInternalServerError, map[string]any{ h.encoder.StatusResponse(w, http.StatusInternalServerError, map[string]any{
"code": "INTERNAL_SERVER_ERROR", "code": "INTERNAL_SERVER_ERROR",
@ -160,13 +149,7 @@ func (h releaseHandler) findRecentReleases(w http.ResponseWriter, r *http.Reques
return return
} }
ret := struct { h.encoder.StatusResponse(w, http.StatusOK, resp)
Data []*domain.Release `json:"data"`
}{
Data: releases,
}
h.encoder.StatusResponse(w, http.StatusOK, ret)
} }
func (h releaseHandler) getReleaseByID(w http.ResponseWriter, r *http.Request) { func (h releaseHandler) getReleaseByID(w http.ResponseWriter, r *http.Request) {

View file

@ -19,8 +19,7 @@ import (
) )
type Service interface { type Service interface {
Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error) Find(ctx context.Context, query domain.ReleaseQueryParams) (*domain.FindReleasesResponse, error)
FindRecent(ctx context.Context) ([]*domain.Release, error)
Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error)
GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error) GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error)
GetIndexerOptions(ctx context.Context) ([]string, error) GetIndexerOptions(ctx context.Context) ([]string, error)
@ -58,14 +57,10 @@ func NewService(log logger.Logger, repo domain.ReleaseRepo, actionSvc action.Ser
} }
} }
func (s *service) Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error) { func (s *service) Find(ctx context.Context, query domain.ReleaseQueryParams) (*domain.FindReleasesResponse, error) {
return s.repo.Find(ctx, query) return s.repo.Find(ctx, query)
} }
func (s *service) FindRecent(ctx context.Context) (res []*domain.Release, err error) {
return s.repo.FindRecent(ctx)
}
func (s *service) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) { func (s *service) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) {
return s.repo.Get(ctx, req) return s.repo.Get(ctx, req)
} }

View file

@ -110,7 +110,7 @@ export const AgeCell = ({value}: CellProps<Release>) => (
</div> </div>
); );
export const IndexerCell = ({value}: CellProps<Release>) => ( export const IndexerCell = (props: CellProps<Release>) => (
<div <div
className={classNames( className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300", "py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
@ -119,11 +119,11 @@ export const IndexerCell = ({value}: CellProps<Release>) => (
> >
<Tooltip <Tooltip
requiresClick requiresClick
label={value} label={props.row.original.indexer.name ? props.row.original.indexer.name : props.row.original.indexer.identifier}
maxWidth="max-w-[90vw]" maxWidth="max-w-[90vw]"
> >
<span className="whitespace-pre-wrap break-words"> <span className="whitespace-pre-wrap break-words">
{value} {props.row.original.indexer.name ? props.row.original.indexer.name : props.row.original.indexer.identifier}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import React, { Suspense, useState } from "react"; import React, { useState } from "react";
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { import {
useTable, useTable,
@ -18,8 +18,8 @@ import { EmptyListState } from "@components/emptystates";
import * as Icons from "@components/Icons"; import * as Icons from "@components/Icons";
import * as DataTable from "@components/data-table"; import * as DataTable from "@components/data-table";
import { RandomLinuxIsos } from "@utils"; import { RandomLinuxIsos } from "@utils";
import { RingResizeSpinner } from "@components/Icons";
import { ReleasesLatestQueryOptions } from "@api/queries"; import { ReleasesLatestQueryOptions } from "@api/queries";
import { IndexerCell } from "@components/data-table";
// This is a custom filter UI for selecting // This is a custom filter UI for selecting
// a unique option from a list // a unique option from a list
@ -166,28 +166,6 @@ function Table({ columns, data }: TableProps) {
); );
} }
export const RecentActivityTable = () => {
return (
<div className="flex flex-col mt-12">
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
Recent activity
</h3>
<div className="animate-pulse text-black dark:text-white">
<Suspense
fallback={
<div className="flex items-center justify-center lg:col-span-9">
<RingResizeSpinner className="text-blue-500 size-12" />
</div>
}
>
{/*<EmptyListState text="Loading..."/>*/}
<ActivityTableContent />
</Suspense>
</div>
</div>
)
}
export const ActivityTable = () => { export const ActivityTable = () => {
const columns = React.useMemo(() => [ const columns = React.useMemo(() => [
{ {
@ -208,7 +186,7 @@ export const ActivityTable = () => {
{ {
Header: "Indexer", Header: "Indexer",
accessor: "indexer.identifier", accessor: "indexer.identifier",
Cell: DataTable.TitleCell, Cell: IndexerCell,
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
filter: "includes" filter: "includes"
} }
@ -275,80 +253,3 @@ export const ActivityTable = () => {
</div> </div>
); );
}; };
export const ActivityTableContent = () => {
const columns = React.useMemo(() => [
{
Header: "Age",
accessor: "timestamp",
Cell: DataTable.AgeCell
},
{
Header: "Release",
accessor: "name",
Cell: DataTable.TitleCell
},
{
Header: "Actions",
accessor: "action_status",
Cell: DataTable.ReleaseStatusCell
},
{
Header: "Indexer",
accessor: "indexer.identifier",
Cell: DataTable.TitleCell,
Filter: SelectColumnFilter,
filter: "includes"
}
] as Column[], []);
const { isLoading, data } = useSuspenseQuery(ReleasesLatestQueryOptions());
const [modifiedData, setModifiedData] = useState<Release[]>([]);
const [showLinuxIsos, setShowLinuxIsos] = useState(false);
if (isLoading) {
return (
<EmptyListState text="Loading..."/>
);
}
const toggleReleaseNames = () => {
setShowLinuxIsos(!showLinuxIsos);
if (!showLinuxIsos && data && data.data) {
const randomNames = RandomLinuxIsos(data.data.length);
const newData: Release[] = data.data.map((item, index) => ({
...item,
name: `${randomNames[index]}.iso`,
indexer: {
id: 0,
name: index % 2 === 0 ? "distrowatch" : "linuxtracker",
identifier: index % 2 === 0 ? "distrowatch" : "linuxtracker",
identifier_external: index % 2 === 0 ? "distrowatch" : "linuxtracker",
},
}));
setModifiedData(newData);
}
};
const displayData = showLinuxIsos ? modifiedData : (data?.data ?? []);
return (
<>
<Table columns={columns} data={displayData} />
<button
onClick={toggleReleaseNames}
className="p-2 absolute -bottom-8 right-0 bg-gray-750 text-white rounded-full opacity-10 hover:opacity-100 transition-opacity duration-300"
aria-label="Toggle view"
title="Go incognito"
>
{showLinuxIsos ? (
<EyeIcon className="h-4 w-4" />
) : (
<EyeSlashIcon className="h-4 w-4" />
)}
</button>
</>
);
};