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"}
)