diff --git a/src/tribler/ui/package-lock.json b/src/tribler/ui/package-lock.json index bfe7783ee1..84240576e5 100644 --- a/src/tribler/ui/package-lock.json +++ b/src/tribler/ui/package-lock.json @@ -29,7 +29,6 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "i18next": "^23.11.4", - "javascript-time-ago": "^2.5.10", "js-cookie": "^3.0.5", "jszip": "^3.10.1", "lucide-react": "^0.292.0", @@ -2869,14 +2868,6 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, - "node_modules/javascript-time-ago": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.10.tgz", - "integrity": "sha512-EUxp4BP74QH8xiYHyeSHopx1XhMMJ9qEX4rcBdFtpVWmKRdzpxbNzz2GSbuekZr5wt0rmLehuyp0PE34EAJT9g==", - "dependencies": { - "relative-time-format": "^1.1.6" - } - }, "node_modules/jiti": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", @@ -3682,11 +3673,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, - "node_modules/relative-time-format": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", - "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5936,14 +5922,6 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, - "javascript-time-ago": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.10.tgz", - "integrity": "sha512-EUxp4BP74QH8xiYHyeSHopx1XhMMJ9qEX4rcBdFtpVWmKRdzpxbNzz2GSbuekZr5wt0rmLehuyp0PE34EAJT9g==", - "requires": { - "relative-time-format": "^1.1.6" - } - }, "jiti": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", @@ -6463,11 +6441,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, - "relative-time-format": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", - "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" - }, "resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/src/tribler/ui/package.json b/src/tribler/ui/package.json index 872065ca6d..ea36dd34f7 100644 --- a/src/tribler/ui/package.json +++ b/src/tribler/ui/package.json @@ -30,7 +30,6 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "i18next": "^23.11.4", - "javascript-time-ago": "^2.5.10", "js-cookie": "^3.0.5", "jszip": "^3.10.1", "lucide-react": "^0.292.0", diff --git a/src/tribler/ui/src/components/swarm-health.tsx b/src/tribler/ui/src/components/swarm-health.tsx index ca08aa3871..0770985979 100644 --- a/src/tribler/ui/src/components/swarm-health.tsx +++ b/src/tribler/ui/src/components/swarm-health.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import toast from 'react-hot-toast'; import { Torrent } from "@/models/torrent.model"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; -import { formatTimeAgo } from "@/lib/utils"; +import { formatTimeRelative } from "@/lib/utils"; import { triblerService } from "@/services/tribler.service"; import { isErrorDict } from "@/services/reporting"; @@ -55,7 +55,7 @@ export function SwarmHealth({ torrent }: { torrent: Torrent }) { - {torrent.last_tracker_check === 0 ? 'Not checked' : `Checked ${formatTimeAgo(torrent.last_tracker_check)}`} + {torrent.last_tracker_check === 0 ? 'Not checked' : `Checked ${formatTimeRelative(torrent.last_tracker_check)}`} diff --git a/src/tribler/ui/src/components/ui/simple-table.tsx b/src/tribler/ui/src/components/ui/simple-table.tsx index 82ae41cbe5..85bb735123 100644 --- a/src/tribler/ui/src/components/ui/simple-table.tsx +++ b/src/tribler/ui/src/components/ui/simple-table.tsx @@ -1,16 +1,25 @@ import { SetStateAction, useEffect, useRef, useState } from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { getCoreRowModel, useReactTable, flexRender, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getSortedRowModel } from '@tanstack/react-table'; -import type { ColumnDef, Row, PaginationState, RowSelectionState, ColumnFiltersState, ExpandedState, ColumnDefTemplate, HeaderContext, SortingState } from '@tanstack/react-table'; +import type { ColumnDef, Row, PaginationState, RowSelectionState, ColumnFiltersState, ExpandedState, ColumnDefTemplate, HeaderContext, SortingState, VisibilityState, Header, Column } from '@tanstack/react-table'; import { cn } from '@/lib/utils'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel } from './select'; import { Button } from './button'; -import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons'; +import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons'; import * as SelectPrimitive from "@radix-ui/react-select" import type { Table as ReactTable } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; import { useResizeObserver } from '@/hooks/useResizeObserver'; import useKeyboardShortcut from 'use-keyboard-shortcut'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from './dropdown-menu'; +import { triblerService } from '@/services/tribler.service'; + + +declare module '@tanstack/table-core/build/lib/types' { + export interface ColumnMeta { + hide_by_default: boolean; + } +} export function getHeader(name: string, translate: boolean = true, addSorting: boolean = true): ColumnDefTemplate> | undefined { @@ -42,15 +51,22 @@ export function getHeader(name: string, translate: boolean = true, addSorting } } -function getStoredSortingState(key?: string) { - if (key) { - let sortingString = localStorage.getItem(key); - if (sortingString) { - return JSON.parse(sortingString); - } +function getState(type: "columns" | "sorting", name?: string) { + let stateString = triblerService.guiSettings[type]; + if (stateString && name) { + return JSON.parse(stateString)[name]; } } +function setState(type: "columns" | "sorting", name: string, state: SortingState | VisibilityState) { + let stateString = triblerService.guiSettings[type]; + let stateSettings = stateString ? JSON.parse(stateString) : {}; + stateSettings[name] = state; + + triblerService.guiSettings[type] = JSON.stringify(stateSettings); + triblerService.setSettings({ ui: triblerService.guiSettings }); +} + interface ReactTableProps { data: T[]; columns: ColumnDef[]; @@ -65,6 +81,7 @@ interface ReactTableProps { allowSelect?: boolean; allowSelectCheckbox?: boolean; allowMultiSelect?: boolean; + allowColumnToggle?: string; filters?: { id: string, value: string }[]; maxHeight?: string | number; expandable?: boolean; @@ -85,6 +102,7 @@ function SimpleTable({ allowSelect, allowSelectCheckbox, allowMultiSelect, + allowColumnToggle, filters, maxHeight, expandable, @@ -98,21 +116,47 @@ function SimpleTable({ const [rowSelection, setRowSelection] = useState(initialRowSelection || {}); const [columnFilters, setColumnFilters] = useState(filters || []) const [expanded, setExpanded] = useState({}); - const [sorting, setSorting] = useState(getStoredSortingState(storeSortingState) || []); + const [sorting, setSorting] = useState(getState("sorting", storeSortingState) || []); - useKeyboardShortcut( - ["Control", "A"], - keys => { - if (allowMultiSelect) { - table.toggleAllRowsSelected(true); - } - }, - { - overrideSystem: true, - ignoreInputFields: true, - repeatOnHold: false + //Get stored column visibility and add missing visibilities with their defaults. + const visibilityState = getState("columns", allowColumnToggle) || {}; + let col: any; + for (col of columns) { + if (col.accessorKey && col.accessorKey in visibilityState === false) { + visibilityState[col.accessorKey] = col.meta?.hide_by_default !== true; + } + } + const [columnVisibility, setColumnVisibility] = useState(visibilityState); + + useKeyboardShortcut(["Control", "A"], () => { + if (allowMultiSelect) { + table.toggleAllRowsSelected(true); } - ); + }, { overrideSystem: true, repeatOnHold: false }); + useKeyboardShortcut(["ArrowUp"], () => { + let ids = Object.keys(rowSelection); + let rows = table.getSortedRowModel().rows; + let index = rows.findIndex((row) => ids.includes(row.id)); + let next = rows[index - 1] || rows[0]; + + let selection: any = {}; + selection[next.id.toString()] = true; + table.setRowSelection(selection); + + document.querySelector("[data-state='selected']")?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); + }); + useKeyboardShortcut(["ArrowDown"], () => { + let ids = Object.keys(rowSelection); + let rows = table.getSortedRowModel().rows; + let index = rows.findLastIndex((row) => ids.includes(row.id)); + let next = rows[index + 1] || rows[rows.length - 1]; + + let selection: any = {}; + selection[next.id.toString()] = true; + table.setRowSelection(selection); + + document.querySelector("[data-state='selected']")?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); + }); const table = useReactTable({ data, @@ -127,11 +171,13 @@ function SimpleTable({ pagination, rowSelection, columnFilters, + columnVisibility, expanded, sorting }, getFilteredRowModel: getFilteredRowModel(), onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, onPaginationChange: setPagination, onRowSelectionChange: (arg: SetStateAction) => { if (allowSelect || allowSelectCheckbox || allowMultiSelect) setRowSelection(arg); @@ -173,10 +219,16 @@ function SimpleTable({ useEffect(() => { if (storeSortingState) { - localStorage.setItem(storeSortingState, JSON.stringify(sorting)); + setState("sorting", storeSortingState, sorting); } }, [sorting]); + useEffect(() => { + if (allowColumnToggle) { + setState("columns", allowColumnToggle, columnVisibility); + } + }, [columnVisibility]); + // For some reason the ScrollArea scrollbar is only shown when it's set to a specific height. // So, we wrap it in a parent div, monitor its size, and set the height of the table accordingly. const parentRef = useRef(null); @@ -186,12 +238,16 @@ function SimpleTable({ <>
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header, index) => { return ( - + {header.isPlaceholder ? null : flexRender( @@ -201,6 +257,41 @@ function SimpleTable({ ) })} + {allowColumnToggle && + + + + + + {t('Toggle columns')} + + {table.getAllLeafColumns().map(column => { + const fakeColumn = { + ...column, + toggleSorting: () => { }, + getIsSorted: () => { }, + } as Column; + return ( + + + + ) + })} + + + } ))} diff --git a/src/tribler/ui/src/lib/utils.ts b/src/tribler/ui/src/lib/utils.ts index a57ca7ea62..4b1745af5d 100644 --- a/src/tribler/ui/src/lib/utils.ts +++ b/src/tribler/ui/src/lib/utils.ts @@ -1,25 +1,10 @@ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" import { category } from "@/models/torrent.model"; -import TimeAgo from 'javascript-time-ago' -import en from 'javascript-time-ago/locale/en' -import es from 'javascript-time-ago/locale/es' -import pt from 'javascript-time-ago/locale/pt' -import ru from 'javascript-time-ago/locale/ru' -import zh from 'javascript-time-ago/locale/zh' -import { useTranslation } from "react-i18next"; -import { triblerService } from "@/services/tribler.service"; import { FileLink, FileTreeItem } from "@/models/file.model"; import { CheckedState } from "@radix-ui/react-checkbox"; import JSZip from "jszip"; - -TimeAgo.setDefaultLocale(en.locale) -TimeAgo.addLocale(en) -TimeAgo.addLocale(es) -TimeAgo.addLocale(pt) -TimeAgo.addLocale(ru) -TimeAgo.addLocale(zh) - +import { triblerService } from "@/services/tribler.service"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -72,10 +57,33 @@ export function categoryIcon(name: category): string { return categoryEmojis[name] || ''; } -export function formatTimeAgo(ts: number) { +export function formatDateTime(ts: number) { + const dtf = new Intl.DateTimeFormat(undefined, { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', hourCycle: "h24", minute: '2-digit', second: '2-digit' + }); + return dtf.format(new Date(ts * 1000)); +} + +export function formatTimeRelative(ts: number, epochTime: boolean = true) { + // Returns passed/future time as human readable text + if (ts === 0) { return '-'; } + if (epochTime) { ts = ts - (Date.now() / 1000); } + const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; + const units: Intl.RelativeTimeFormatUnit[] = ["second", "minute", "hour", "day", "week", "month", "year"]; + const index = cutoffs.findIndex(cutoff => cutoff > Math.abs(ts)); + const divisor = index ? cutoffs[index - 1] : 1; let locale = triblerService.guiSettings.lang ?? 'en_US'; - const timeAg = new TimeAgo(locale.slice(0, 2)); - return timeAg.format(ts * 1000); + const rtf = new Intl.RelativeTimeFormat(locale.replace("_", "-"), { numeric: "auto" }); + return divisor === Infinity ? "-" : rtf.format(Math.round(ts / divisor), units[index]); +} + +export function formatTimeRelativeISO(ts: number) { + // Returns passed time as HH:mm:ss + if (ts === 0) { return '-'; } + const date = new Date(0); + date.setSeconds((Date.now() / 1000) - ts); + return date.toISOString().substr(11, 8); } export function formatBytes(bytes: number) { @@ -84,19 +92,6 @@ export function formatBytes(bytes: number) { return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B'; } -export function formatTimeDiff(time: number) { - if (time === 0) { return '-'; } - const now = Date.now() / 1000; - return formatTime(now - time); -} - -export function formatTime(time: number) { - if (time === 0) { return '-'; } - const date = new Date(0); - date.setSeconds(time); - return date.toISOString().substr(11, 8); -} - export function formatFlags(flags: number[]) { const flagToString: Record = { 1: 'RELAY', diff --git a/src/tribler/ui/src/models/settings.model.tsx b/src/tribler/ui/src/models/settings.model.tsx index 8c8b4cc2dc..7e8998e2cd 100644 --- a/src/tribler/ui/src/models/settings.model.tsx +++ b/src/tribler/ui/src/models/settings.model.tsx @@ -105,4 +105,6 @@ export interface GuiSettings { dev_mode?: boolean; lang?: string; theme?: string; + sorting?: string; + columns?: string; } diff --git a/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx b/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx index 12964a3999..e5625b376e 100644 --- a/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx +++ b/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx @@ -5,7 +5,7 @@ import { ipv8Service } from "@/services/ipv8.service"; import { isErrorDict } from "@/services/reporting"; import { Task } from "@/models/task.model"; import { useInterval } from '@/hooks/useInterval'; -import { formatTimeDiff } from "@/lib/utils"; +import { formatTimeRelativeISO } from "@/lib/utils"; const taskColumns: ColumnDef[] = [ @@ -32,7 +32,7 @@ const taskColumns: ColumnDef[] = [ accessorKey: "start_time", header: getHeader("Started", false), cell: ({ row }) => { - return row.original.start_time && {formatTimeDiff(row.original.start_time)} + return row.original.start_time && {formatTimeRelativeISO(row.original.start_time)} }, }, ] diff --git a/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx b/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx index 9ba1507b06..d2441df83f 100644 --- a/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx +++ b/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx @@ -4,7 +4,7 @@ import { ipv8Service } from "@/services/ipv8.service"; import { isErrorDict } from "@/services/reporting"; import { Bucket } from "@/models/bucket.model"; import { ColumnDef } from "@tanstack/react-table"; -import { formatTimeDiff } from "@/lib/utils"; +import { formatTimeRelativeISO } from "@/lib/utils"; import { useInterval } from '@/hooks/useInterval'; @@ -17,7 +17,7 @@ const bucketColumns: ColumnDef[] = [ accessorKey: "last_changed", header: getHeader("Last changed", false), cell: ({ row }) => { - return {formatTimeDiff(row.original.last_changed)} + return {formatTimeRelativeISO(row.original.last_changed)} }, }, { diff --git a/src/tribler/ui/src/pages/Debug/IPv8/index.tsx b/src/tribler/ui/src/pages/Debug/IPv8/index.tsx index 41dab22441..e564742d50 100644 --- a/src/tribler/ui/src/pages/Debug/IPv8/index.tsx +++ b/src/tribler/ui/src/pages/Debug/IPv8/index.tsx @@ -10,7 +10,7 @@ export default function IPv8() { Overlays Details - + diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx index 5855d7a782..9d4f3f5218 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx @@ -4,7 +4,7 @@ import { ipv8Service } from "@/services/ipv8.service"; import { isErrorDict } from "@/services/reporting"; import { Circuit } from "@/models/circuit.model"; import { ColumnDef } from "@tanstack/react-table"; -import { formatBytes, formatFlags, formatTimeDiff } from "@/lib/utils"; +import { formatBytes, formatFlags, formatTimeRelativeISO } from "@/lib/utils"; import { useInterval } from '@/hooks/useInterval'; @@ -46,7 +46,7 @@ const circuitColumns: ColumnDef[] = [ accessorKey: "uptime", header: getHeader("Uptime", false), cell: ({ row }) => { - return {formatTimeDiff(row.original.creation_time)} + return {formatTimeRelativeISO(row.original.creation_time)} }, }, { diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx index 17a206ffb9..78b8a2d7fe 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx @@ -4,7 +4,7 @@ import { ipv8Service } from "@/services/ipv8.service"; import { isErrorDict } from "@/services/reporting"; import { Exit } from "@/models/exit.model"; import { ColumnDef } from "@tanstack/react-table"; -import { formatBytes, formatTimeDiff } from "@/lib/utils"; +import { formatBytes, formatTimeRelativeISO } from "@/lib/utils"; import { useInterval } from '@/hooks/useInterval'; @@ -35,7 +35,7 @@ const exitColumns: ColumnDef[] = [ accessorKey: "uptime", header: getHeader("Uptime", false), cell: ({ row }) => { - return {formatTimeDiff(row.original.creation_time)} + return {formatTimeRelativeISO(row.original.creation_time)} }, }, ] diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx index 0b7436c373..79f7cff67d 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; import { isErrorDict } from "@/services/reporting"; import { Relay } from "@/models/relay.model"; -import { formatBytes, formatTimeDiff } from "@/lib/utils"; +import { formatBytes, formatTimeRelativeISO } from "@/lib/utils"; import { useInterval } from '@/hooks/useInterval'; @@ -39,7 +39,7 @@ const relayColumns: ColumnDef[] = [ accessorKey: "uptime", header: getHeader("Uptime", false), cell: ({ row }) => { - return {formatTimeDiff(row.original.creation_time)} + return {formatTimeRelativeISO(row.original.creation_time)} }, }, ] diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx index 40a1586715..4fd2ca9e8c 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx @@ -4,7 +4,7 @@ import { ipv8Service } from "@/services/ipv8.service"; import { isErrorDict } from "@/services/reporting"; import { Swarm } from "@/models/swarm.model"; import { ColumnDef } from "@tanstack/react-table"; -import { formatBytes, formatTimeDiff } from "@/lib/utils"; +import { formatBytes, formatTimeRelativeISO } from "@/lib/utils"; import { useInterval } from '@/hooks/useInterval'; @@ -33,7 +33,7 @@ const swarmColumns: ColumnDef[] = [ accessorKey: "last_lookup", header: getHeader("Last lookup", false), cell: ({ row }) => { - return {formatTimeDiff(row.original.last_lookup)} + return {formatTimeRelativeISO(row.original.last_lookup)} }, }, { diff --git a/src/tribler/ui/src/pages/Downloads/index.tsx b/src/tribler/ui/src/pages/Downloads/index.tsx index 44f37e6d06..6629e0fbe6 100644 --- a/src/tribler/ui/src/pages/Downloads/index.tsx +++ b/src/tribler/ui/src/pages/Downloads/index.tsx @@ -3,7 +3,7 @@ import DownloadDetails from "./Details"; import SimpleTable, { getHeader } from "@/components/ui/simple-table" import { Download } from "@/models/download.model"; import { Progress } from "@/components/ui/progress" -import { capitalize, formatBytes } from "@/lib/utils"; +import { capitalize, formatBytes, formatDateTime, formatTimeRelative } from "@/lib/utils"; import { isErrorDict } from "@/services/reporting"; import { triblerService } from "@/services/tribler.service"; import { ColumnDef } from "@tanstack/react-table" @@ -92,6 +92,28 @@ const downloadColumns: ColumnDef[] = [ accessorKey: "hops", header: getHeader('Hops'), }, + { + accessorKey: "eta", + header: getHeader('ETA'), + meta: { + hide_by_default: true, + }, + cell: ({ row }) => { + if (row.original.progress === 1 || row.original.status_code !== 3) + return - + return {formatTimeRelative(row.original.eta, false)} + }, + }, + { + accessorKey: "time_added", + header: getHeader('AddedOn'), + meta: { + hide_by_default: true, + }, + cell: ({ row }) => { + return {formatDateTime(row.original.time_added)} + }, + }, ] export default function Downloads({ statusFilter }: { statusFilter: number[] }) { @@ -194,6 +216,7 @@ export default function Downloads({ statusFilter }: { statusFilter: number[] }) allowMultiSelect={true} onSelectedRowsChange={setSelectedDownloads} maxHeight={Math.max((parentRect?.height ?? 50) - 50, 50)} + allowColumnToggle="download-columns" storeSortingState="download-sorting" rowId={(row) => row.infohash} /> diff --git a/src/tribler/ui/src/pages/Popular/index.tsx b/src/tribler/ui/src/pages/Popular/index.tsx index 72e40fcc89..fdcc7bb309 100644 --- a/src/tribler/ui/src/pages/Popular/index.tsx +++ b/src/tribler/ui/src/pages/Popular/index.tsx @@ -5,7 +5,7 @@ import { triblerService } from "@/services/tribler.service"; import { isErrorDict } from "@/services/reporting"; import { Torrent } from "@/models/torrent.model"; import { ColumnDef } from "@tanstack/react-table"; -import { categoryIcon, filterDuplicates, formatBytes, formatTimeAgo, getMagnetLink } from "@/lib/utils"; +import { categoryIcon, filterDuplicates, formatBytes, formatTimeRelative, getMagnetLink } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useInterval } from '@/hooks/useInterval'; import { SwarmHealth } from "@/components/swarm-health"; @@ -53,7 +53,7 @@ const getColumns = ({ onDownload }: { onDownload: (torrent: Torrent) => void }): return ( {row.original.created > 24 * 3600 ? - formatTimeAgo(row.original.created) : + formatTimeRelative(row.original.created) : "unknown"} ) diff --git a/src/tribler/ui/src/pages/Search/index.tsx b/src/tribler/ui/src/pages/Search/index.tsx index d1a47ad857..81ccbddea9 100644 --- a/src/tribler/ui/src/pages/Search/index.tsx +++ b/src/tribler/ui/src/pages/Search/index.tsx @@ -4,7 +4,7 @@ import { triblerService } from "@/services/tribler.service"; import { isErrorDict } from "@/services/reporting"; import { Torrent } from "@/models/torrent.model"; import { ColumnDef } from "@tanstack/react-table"; -import { categoryIcon, filterDuplicates, formatBytes, formatTimeAgo, getMagnetLink } from "@/lib/utils"; +import { categoryIcon, filterDuplicates, formatBytes, formatTimeRelative, getMagnetLink } from "@/lib/utils"; import SaveAs from "@/dialogs/SaveAs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useSearchParams } from "react-router-dom"; @@ -53,7 +53,7 @@ const getColumns = ({ onDownload }: { onDownload: (torrent: Torrent) => void }): return ( {row.original.created > 24 * 3600 ? - formatTimeAgo(row.original.created) : + formatTimeRelative(row.original.created) : "unknown"} )