Merge branch 'dev' of https://github.com/vffuunnyy/clash-verge-rev-lite into dev
* 'dev' of https://github.com/vffuunnyy/clash-verge-rev-lite: the Add Profile button has been moved, and the layout has been slightly changed. the connections page has been slightly revised. fixed an issue with the dialog box when the profile name is long. added glass effect to components fixed icon background
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 85 KiB |
@@ -1,15 +1,14 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import React, { useMemo, useState, useEffect, RefObject } from "react";
|
import React, { useMemo, useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
Row,
|
Header,
|
||||||
ColumnSizingState,
|
ColumnSizingState,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { TableVirtuoso, TableComponents } from "react-virtuoso";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -27,7 +26,30 @@ import { cn } from "@root/lib/utils";
|
|||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
// Интерфейс для строки данных, которую использует react-table
|
interface IConnectionsItem {
|
||||||
|
id: string;
|
||||||
|
metadata: {
|
||||||
|
host: string;
|
||||||
|
destinationIP: string;
|
||||||
|
destinationPort: string;
|
||||||
|
remoteDestination: string;
|
||||||
|
process?: string;
|
||||||
|
processPath?: string;
|
||||||
|
sourceIP: string;
|
||||||
|
sourcePort: string;
|
||||||
|
type: string;
|
||||||
|
network: string;
|
||||||
|
};
|
||||||
|
rule: string;
|
||||||
|
rulePayload?: string;
|
||||||
|
chains: string[];
|
||||||
|
download: number;
|
||||||
|
upload: number;
|
||||||
|
curDownload?: number;
|
||||||
|
curUpload?: number;
|
||||||
|
start: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ConnectionRow {
|
interface ConnectionRow {
|
||||||
id: string;
|
id: string;
|
||||||
host: string;
|
host: string;
|
||||||
@@ -45,29 +67,81 @@ interface ConnectionRow {
|
|||||||
connectionData: IConnectionsItem;
|
connectionData: IConnectionsItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Интерфейс для пропсов, которые компонент получает от родителя
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: IConnectionsItem[];
|
connections: IConnectionsItem[];
|
||||||
onShowDetail: (data: IConnectionsItem) => void;
|
onShowDetail: (data: IConnectionsItem) => void;
|
||||||
scrollerRef: (element: HTMLElement | Window | null) => void;
|
scrollerRef: (element: HTMLElement | Window | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ColumnResizer = ({ header }: { header: Header<ConnectionRow, unknown> }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseDown={header.getResizeHandler()}
|
||||||
|
onTouchStart={header.getResizeHandler()}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none",
|
||||||
|
"bg-transparent hover:bg-primary/50 active:bg-primary",
|
||||||
|
"transition-colors duration-150",
|
||||||
|
header.column.getIsResizing() && "bg-primary"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transform: header.column.getIsResizing() ? `translateX(0px)` : "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ConnectionTable = (props: Props) => {
|
export const ConnectionTable = (props: Props) => {
|
||||||
const { connections, onShowDetail, scrollerRef } = props;
|
const { connections, onShowDetail, scrollerRef } = props;
|
||||||
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableContainerRef.current && scrollerRef) {
|
||||||
|
scrollerRef(tableContainerRef.current);
|
||||||
|
}
|
||||||
|
}, [scrollerRef]);
|
||||||
|
|
||||||
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(() => {
|
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(() => {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("connection-table-widths");
|
const saved = localStorage.getItem("connection-table-widths");
|
||||||
return saved ? JSON.parse(saved) : {};
|
return saved
|
||||||
|
? JSON.parse(saved)
|
||||||
|
: {
|
||||||
|
host: 220,
|
||||||
|
download: 88,
|
||||||
|
upload: 88,
|
||||||
|
dlSpeed: 88,
|
||||||
|
ulSpeed: 88,
|
||||||
|
chains: 340,
|
||||||
|
rule: 280,
|
||||||
|
process: 220,
|
||||||
|
time: 120,
|
||||||
|
source: 200,
|
||||||
|
remoteDestination: 200,
|
||||||
|
type: 160,
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {
|
||||||
|
host: 220,
|
||||||
|
download: 88,
|
||||||
|
upload: 88,
|
||||||
|
dlSpeed: 88,
|
||||||
|
ulSpeed: 88,
|
||||||
|
chains: 340,
|
||||||
|
rule: 280,
|
||||||
|
process: 220,
|
||||||
|
time: 120,
|
||||||
|
source: 200,
|
||||||
|
remoteDestination: 200,
|
||||||
|
type: 160,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"connection-table-widths",
|
"connection-table-widths",
|
||||||
JSON.stringify(columnSizing),
|
JSON.stringify(columnSizing)
|
||||||
);
|
);
|
||||||
}, [columnSizing]);
|
}, [columnSizing]);
|
||||||
|
|
||||||
@@ -107,13 +181,16 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
header: () => t("Host"),
|
header: () => t("Host"),
|
||||||
size: columnSizing?.host || 220,
|
size: columnSizing?.host || 220,
|
||||||
minSize: 180,
|
minSize: 180,
|
||||||
|
maxSize: 400,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "download",
|
accessorKey: "download",
|
||||||
header: () => t("Downloaded"),
|
header: () => t("Downloaded"),
|
||||||
size: columnSizing?.download || 88,
|
size: columnSizing?.download || 88,
|
||||||
|
minSize: 80,
|
||||||
|
maxSize: 150,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<div className="text-right">
|
<div className="text-right font-mono text-sm">
|
||||||
{parseTraffic(getValue<number>()).join(" ")}
|
{parseTraffic(getValue<number>()).join(" ")}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -122,8 +199,10 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
accessorKey: "upload",
|
accessorKey: "upload",
|
||||||
header: () => t("Uploaded"),
|
header: () => t("Uploaded"),
|
||||||
size: columnSizing?.upload || 88,
|
size: columnSizing?.upload || 88,
|
||||||
|
minSize: 80,
|
||||||
|
maxSize: 150,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<div className="text-right">
|
<div className="text-right font-mono text-sm">
|
||||||
{parseTraffic(getValue<number>()).join(" ")}
|
{parseTraffic(getValue<number>()).join(" ")}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -132,8 +211,10 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
accessorKey: "dlSpeed",
|
accessorKey: "dlSpeed",
|
||||||
header: () => t("DL Speed"),
|
header: () => t("DL Speed"),
|
||||||
size: columnSizing?.dlSpeed || 88,
|
size: columnSizing?.dlSpeed || 88,
|
||||||
|
minSize: 80,
|
||||||
|
maxSize: 150,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<div className="text-right">
|
<div className="text-right font-mono text-sm">
|
||||||
{parseTraffic(getValue<number>()).join(" ")}/s
|
{parseTraffic(getValue<number>()).join(" ")}/s
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -142,8 +223,10 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
accessorKey: "ulSpeed",
|
accessorKey: "ulSpeed",
|
||||||
header: () => t("UL Speed"),
|
header: () => t("UL Speed"),
|
||||||
size: columnSizing?.ulSpeed || 88,
|
size: columnSizing?.ulSpeed || 88,
|
||||||
|
minSize: 80,
|
||||||
|
maxSize: 150,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<div className="text-right">
|
<div className="text-right font-mono text-sm">
|
||||||
{parseTraffic(getValue<number>()).join(" ")}/s
|
{parseTraffic(getValue<number>()).join(" ")}/s
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -153,26 +236,30 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
header: () => t("Chains"),
|
header: () => t("Chains"),
|
||||||
size: columnSizing?.chains || 340,
|
size: columnSizing?.chains || 340,
|
||||||
minSize: 180,
|
minSize: 180,
|
||||||
|
maxSize: 500,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "rule",
|
accessorKey: "rule",
|
||||||
header: () => t("Rule"),
|
header: () => t("Rule"),
|
||||||
size: columnSizing?.rule || 280,
|
size: columnSizing?.rule || 280,
|
||||||
minSize: 180,
|
minSize: 180,
|
||||||
|
maxSize: 400,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "process",
|
accessorKey: "process",
|
||||||
header: () => t("Process"),
|
header: () => t("Process"),
|
||||||
size: columnSizing?.process || 220,
|
size: columnSizing?.process || 220,
|
||||||
minSize: 180,
|
minSize: 180,
|
||||||
|
maxSize: 350,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "time",
|
accessorKey: "time",
|
||||||
header: () => t("Time"),
|
header: () => t("Time"),
|
||||||
size: columnSizing?.time || 120,
|
size: columnSizing?.time || 120,
|
||||||
minSize: 100,
|
minSize: 100,
|
||||||
|
maxSize: 180,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => (
|
||||||
<div className="text-right">
|
<div className="text-right font-mono text-sm">
|
||||||
{dayjs(getValue<string>()).fromNow()}
|
{dayjs(getValue<string>()).fromNow()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -182,21 +269,24 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
header: () => t("Source"),
|
header: () => t("Source"),
|
||||||
size: columnSizing?.source || 200,
|
size: columnSizing?.source || 200,
|
||||||
minSize: 130,
|
minSize: 130,
|
||||||
|
maxSize: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "remoteDestination",
|
accessorKey: "remoteDestination",
|
||||||
header: () => t("Destination"),
|
header: () => t("Destination"),
|
||||||
size: columnSizing?.remoteDestination || 200,
|
size: columnSizing?.remoteDestination || 200,
|
||||||
minSize: 130,
|
minSize: 130,
|
||||||
|
maxSize: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "type",
|
accessorKey: "type",
|
||||||
header: () => t("Type"),
|
header: () => t("Type"),
|
||||||
size: columnSizing?.type || 160,
|
size: columnSizing?.type || 160,
|
||||||
minSize: 100,
|
minSize: 100,
|
||||||
|
maxSize: 220,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[columnSizing],
|
[columnSizing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
@@ -206,92 +296,91 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
onColumnSizingChange: setColumnSizing,
|
onColumnSizingChange: setColumnSizing,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
columnResizeMode: "onChange",
|
columnResizeMode: "onChange",
|
||||||
|
enableColumnResizing: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const VirtuosoTableComponents = useMemo<TableComponents<Row<ConnectionRow>>>(
|
const totalTableWidth = useMemo(() => {
|
||||||
() => ({
|
return table.getCenterTotalSize();
|
||||||
// Явно типизируем `ref` для каждого компонента
|
}, [table.getState().columnSizing]);
|
||||||
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
|
|
||||||
<div className="h-full" {...props} ref={ref} />
|
if (connRows.length === 0) {
|
||||||
)),
|
|
||||||
Table: (props) => <Table {...props} className="w-full border-collapse" />,
|
|
||||||
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
|
|
||||||
<TableHeader {...props} ref={ref} />
|
|
||||||
)),
|
|
||||||
// Явно типизируем пропсы и `ref` для TableRow
|
|
||||||
TableRow: React.forwardRef<
|
|
||||||
HTMLTableRowElement,
|
|
||||||
{ item: Row<ConnectionRow> } & React.HTMLAttributes<HTMLTableRowElement>
|
|
||||||
>(({ item: row, ...props }, ref) => {
|
|
||||||
// `Virtuoso` передает нам готовую строку `row` в пропсе `item`.
|
|
||||||
// Больше не нужно искать ее по индексу!
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<div className="flex h-full items-center justify-center">
|
||||||
{...props}
|
<p className="text-muted-foreground">{t("No connections")}</p>
|
||||||
ref={ref}
|
</div>
|
||||||
data-state={row.getIsSelected() && "selected"}
|
|
||||||
className="cursor-pointer hover:bg-muted/50"
|
|
||||||
onClick={() => onShowDetail(row.original.connectionData)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
|
|
||||||
<TableBody {...props} ref={ref} />
|
|
||||||
)),
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full rounded-md border overflow-hidden">
|
<div className="rounded-md border relative bg-background">
|
||||||
{connRows.length > 0 ? (
|
<Table
|
||||||
<TableVirtuoso
|
className="w-full border-collapse table-fixed"
|
||||||
scrollerRef={scrollerRef}
|
style={{
|
||||||
data={table.getRowModel().rows}
|
width: totalTableWidth,
|
||||||
components={VirtuosoTableComponents}
|
minWidth: "100%",
|
||||||
fixedHeaderContent={() =>
|
}}
|
||||||
table.getHeaderGroups().map((headerGroup) => (
|
>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={headerGroup.id}
|
key={headerGroup.id}
|
||||||
className="hover:bg-transparent bg-background/95 backdrop-blur"
|
className="hover:bg-transparent border-b-0 h-10"
|
||||||
>
|
>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={header.id}
|
key={header.id}
|
||||||
style={{ width: header.getSize() }}
|
className={cn(
|
||||||
className="p-2"
|
"sticky top-0 z-10",
|
||||||
|
"p-2 text-xs font-semibold select-none border-r last:border-r-0 bg-background h-10"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: header.getSize(),
|
||||||
|
minWidth: header.column.columnDef.minSize,
|
||||||
|
maxWidth: header.column.columnDef.maxSize,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center justify-between h-full">
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext(),
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{header.column.getCanResize() && (
|
||||||
|
<ColumnResizer header={header} />
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
}
|
</TableHeader>
|
||||||
itemContent={(index, row) => (
|
|
||||||
<>
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
|
onClick={() => onShowDetail(row.original.connectionData)}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
style={{ width: cell.column.getSize() }}
|
className="p-2 whitespace-nowrap overflow-hidden text-ellipsis text-sm border-r last:border-r-0"
|
||||||
className="p-2 whitespace-nowrap"
|
style={{
|
||||||
onClick={() => onShowDetail(row.original.connectionData)}
|
width: cell.column.getSize(),
|
||||||
|
minWidth: cell.column.columnDef.minSize,
|
||||||
|
maxWidth: cell.column.columnDef.maxSize,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</>
|
</TableRow>
|
||||||
)}
|
))}
|
||||||
/>
|
</TableBody>
|
||||||
) : (
|
</Table>
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<p>No results.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
// Новые импорты из shadcn/ui
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -18,7 +17,7 @@ interface Props {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
onOpenChange: (open: boolean) => void; // shadcn использует этот коллбэк
|
onOpenChange: (open: boolean) => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ export const ConfirmViewer = (props: Props) => {
|
|||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
<AlertDialogTitle className="truncate">{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { open } from "@tauri-apps/plugin-shell";
|
|||||||
import { ProxiesEditorViewer } from "./proxies-editor-viewer";
|
import { ProxiesEditorViewer } from "./proxies-editor-viewer";
|
||||||
import { cn } from "@root/lib/utils";
|
import { cn } from "@root/lib/utils";
|
||||||
|
|
||||||
// --- Компоненты shadcn/ui ---
|
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -46,7 +45,6 @@ import {
|
|||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
|
||||||
// --- Иконки ---
|
|
||||||
import {
|
import {
|
||||||
GripVertical,
|
GripVertical,
|
||||||
File as FileIcon,
|
File as FileIcon,
|
||||||
@@ -71,10 +69,8 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
// Активируем плагин для dayjs
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
// --- Вспомогательные функции ---
|
|
||||||
const parseUrl = (url?: string): string | undefined => {
|
const parseUrl = (url?: string): string | undefined => {
|
||||||
if (!url) return undefined;
|
if (!url) return undefined;
|
||||||
try {
|
try {
|
||||||
@@ -302,6 +298,11 @@ export const ProfileItem = (props: Props) => {
|
|||||||
isDestructive: true,
|
isDestructive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_NAME_LENGTH = 25;
|
||||||
|
const truncatedName = name.length > MAX_NAME_LENGTH
|
||||||
|
? `${name.slice(0, MAX_NAME_LENGTH)}...`
|
||||||
|
: name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setNodeRef} style={style} {...attributes}>
|
<div ref={setNodeRef} style={style} {...attributes}>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
@@ -459,7 +460,6 @@ export const ProfileItem = (props: Props) => {
|
|||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
||||||
{/* Модальные окна для редактирования */}
|
|
||||||
{fileOpen && (
|
{fileOpen && (
|
||||||
<EditorViewer
|
<EditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
@@ -479,10 +479,10 @@ export const ProfileItem = (props: Props) => {
|
|||||||
<RulesEditorViewer
|
<RulesEditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
onClose={() => setRulesOpen(false)}
|
onClose={() => setRulesOpen(false)}
|
||||||
profileUid={uid} // <-- Был 'uid', стал 'profileUid'
|
profileUid={uid}
|
||||||
property={option?.rules ?? ""}
|
property={option?.rules ?? ""}
|
||||||
groupsUid={option?.groups ?? ""} // <-- Добавлен недостающий пропс
|
groupsUid={option?.groups ?? ""}
|
||||||
mergeUid={option?.merge ?? ""} // <-- Добавлен недостающий пропс
|
mergeUid={option?.merge ?? ""}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -491,7 +491,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
<ProxiesEditorViewer
|
<ProxiesEditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
onClose={() => setProxiesOpen(false)}
|
onClose={() => setProxiesOpen(false)}
|
||||||
profileUid={uid} // <-- Был 'uid', стал 'profileUid'
|
profileUid={uid}
|
||||||
property={option?.proxies ?? ""}
|
property={option?.proxies ?? ""}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
@@ -501,10 +501,10 @@ export const ProfileItem = (props: Props) => {
|
|||||||
<GroupsEditorViewer
|
<GroupsEditorViewer
|
||||||
open={true}
|
open={true}
|
||||||
onClose={() => setGroupsOpen(false)}
|
onClose={() => setGroupsOpen(false)}
|
||||||
profileUid={uid} // <-- Был 'uid', стал 'profileUid'
|
profileUid={uid}
|
||||||
property={option?.groups ?? ""}
|
property={option?.groups ?? ""}
|
||||||
proxiesUid={option?.proxies ?? ""} // <-- Добавлен недостающий пропс
|
proxiesUid={option?.proxies ?? ""}
|
||||||
mergeUid={option?.merge ?? ""} // <-- Добавлен недостающий пропс
|
mergeUid={option?.merge ?? ""}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -513,7 +513,7 @@ export const ProfileItem = (props: Props) => {
|
|||||||
open={confirmOpen}
|
open={confirmOpen}
|
||||||
onOpenChange={setConfirmOpen}
|
onOpenChange={setConfirmOpen}
|
||||||
onConfirm={onDelete}
|
onConfirm={onDelete}
|
||||||
title={t("Delete Profile", { name })}
|
title={t("Delete Profile", { name: truncatedName })}
|
||||||
description={t("This action cannot be undone.")}
|
description={t("This action cannot be undone.")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ function DropdownMenuContent({
|
|||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md p-1",
|
||||||
|
"backdrop-blur-sm bg-white/70 border border-white/40",
|
||||||
|
"dark:bg-white/10 dark:border-white/20",
|
||||||
|
"shadow-[0_8px_32px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_32px_rgba(255,255,255,0.05)]",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -72,7 +76,13 @@ function DropdownMenuItem({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8",
|
||||||
|
"hover:backdrop-blur-sm hover:bg-gray-200/80 focus:backdrop-blur-sm focus:bg-gray-200/80",
|
||||||
|
"dark:hover:bg-white/20 dark:focus:bg-white/20",
|
||||||
|
"data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-red-200/80 data-[variant=destructive]:focus:bg-red-200/80",
|
||||||
|
"dark:data-[variant=destructive]:hover:bg-destructive/20 dark:data-[variant=destructive]:focus:bg-destructive/20",
|
||||||
|
"data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive",
|
||||||
|
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -90,7 +100,10 @@ function DropdownMenuCheckboxItem({
|
|||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
"hover:backdrop-blur-sm hover:bg-gray-200/80 focus:backdrop-blur-sm focus:bg-gray-200/80",
|
||||||
|
"dark:hover:bg-white/20 dark:focus:bg-white/20",
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@@ -126,7 +139,10 @@ function DropdownMenuRadioItem({
|
|||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
"hover:backdrop-blur-sm hover:bg-gray-200/80 focus:backdrop-blur-sm focus:bg-gray-200/80",
|
||||||
|
"dark:hover:bg-white/20 dark:focus:bg-white/20",
|
||||||
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -154,6 +170,7 @@ function DropdownMenuLabel({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
"text-foreground/80",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -168,7 +185,11 @@ function DropdownMenuSeparator({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn(
|
||||||
|
"-mx-1 my-1 h-px",
|
||||||
|
"bg-gray-400/50 dark:bg-white/20",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -182,7 +203,8 @@ function DropdownMenuShortcut({
|
|||||||
<span
|
<span
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"ml-auto text-xs tracking-widest",
|
||||||
|
"text-foreground/60",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -209,7 +231,10 @@ function DropdownMenuSubTrigger({
|
|||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||||
|
"hover:backdrop-blur-sm hover:bg-gray-200/80 focus:backdrop-blur-sm focus:bg-gray-200/80",
|
||||||
|
"data-[state=open]:backdrop-blur-sm data-[state=open]:bg-gray-200/80",
|
||||||
|
"dark:hover:bg-white/20 dark:focus:bg-white/20 dark:data-[state=open]:bg-white/20",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -228,7 +253,11 @@ function DropdownMenuSubContent({
|
|||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md p-1",
|
||||||
|
"backdrop-blur-sm bg-white/70 border border-white/40",
|
||||||
|
"dark:bg-white/10 dark:border-white/20",
|
||||||
|
"shadow-[0_8px_32px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_32px_rgba(255,255,255,0.05)]",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ function SelectTrigger({
|
|||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow,background-color] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
"backdrop-blur-sm bg-white/80 border-gray-300/60",
|
||||||
|
"dark:bg-white/5 dark:border-white/15",
|
||||||
|
"hover:bg-white/90 hover:border-gray-400/70",
|
||||||
|
"dark:hover:bg-white/10 dark:hover:border-white/20",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -59,7 +63,11 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md",
|
||||||
|
"backdrop-blur-sm bg-white/70 border border-white/40",
|
||||||
|
"dark:bg-white/10 dark:border-white/20",
|
||||||
|
"shadow-[0_8px_32px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_32px_rgba(255,255,255,0.05)]",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className,
|
className,
|
||||||
@@ -105,7 +113,11 @@ function SelectItem({
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
"hover:backdrop-blur-sm hover:bg-gray-200/80 focus:backdrop-blur-sm focus:bg-gray-200/80",
|
||||||
|
"dark:hover:bg-white/20 dark:focus:bg-white/20",
|
||||||
|
"transition-all duration-200",
|
||||||
|
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -49,18 +49,37 @@ import {
|
|||||||
PauseCircle,
|
PauseCircle,
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
Menu,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {SidebarTrigger} from "@/components/ui/sidebar";
|
import {SidebarTrigger} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
|
interface IConnectionsItem {
|
||||||
|
id: string;
|
||||||
|
metadata: {
|
||||||
|
host: string;
|
||||||
|
destinationIP: string;
|
||||||
|
process?: string;
|
||||||
|
};
|
||||||
|
start?: string;
|
||||||
|
curUpload?: number;
|
||||||
|
curDownload?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IConnections {
|
||||||
|
uploadTotal: number;
|
||||||
|
downloadTotal: number;
|
||||||
|
connections: IConnectionsItem[];
|
||||||
|
data: IConnectionsItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
||||||
|
|
||||||
const initConn: IConnections = {
|
const initConn: IConnections = {
|
||||||
uploadTotal: 0,
|
uploadTotal: 0,
|
||||||
downloadTotal: 0,
|
downloadTotal: 0,
|
||||||
connections: [],
|
connections: [],
|
||||||
|
data: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
|
||||||
|
|
||||||
const ConnectionsPage = () => {
|
const ConnectionsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
@@ -77,9 +96,9 @@ const ConnectionsPage = () => {
|
|||||||
new Date(b.start || "0").getTime()! -
|
new Date(b.start || "0").getTime()! -
|
||||||
new Date(a.start || "0").getTime()!,
|
new Date(a.start || "0").getTime()!,
|
||||||
),
|
),
|
||||||
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
|
"Upload Speed": (list) => list.sort((a, b) => (b.curUpload ?? 0) - (a.curUpload ?? 0)),
|
||||||
"Download Speed": (list) =>
|
"Download Speed": (list) =>
|
||||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
list.sort((a, b) => (b.curDownload ?? 0) - (a.curDownload ?? 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
@@ -91,6 +110,7 @@ const ConnectionsPage = () => {
|
|||||||
uploadTotal: connections.uploadTotal,
|
uploadTotal: connections.uploadTotal,
|
||||||
downloadTotal: connections.downloadTotal,
|
downloadTotal: connections.downloadTotal,
|
||||||
connections: connections.data,
|
connections: connections.data,
|
||||||
|
data: connections.data,
|
||||||
};
|
};
|
||||||
if (isPaused) return frozenData ?? currentData;
|
if (isPaused) return frozenData ?? currentData;
|
||||||
return currentData;
|
return currentData;
|
||||||
@@ -147,6 +167,7 @@ const ConnectionsPage = () => {
|
|||||||
uploadTotal: connections.uploadTotal,
|
uploadTotal: connections.uploadTotal,
|
||||||
downloadTotal: connections.downloadTotal,
|
downloadTotal: connections.downloadTotal,
|
||||||
connections: connections.data,
|
connections: connections.data,
|
||||||
|
data: connections.data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFrozenData(null);
|
setFrozenData(null);
|
||||||
@@ -155,13 +176,13 @@ const ConnectionsPage = () => {
|
|||||||
});
|
});
|
||||||
}, [connections]);
|
}, [connections]);
|
||||||
|
|
||||||
|
const headerHeight = "7rem";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative">
|
<div className="relative h-full w-full">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className="absolute top-0 left-0 right-0 z-20 p-4 bg-background/80 backdrop-blur-sm"
|
||||||
"absolute top-0 left-0 right-0 z-10 p-4 transition-all duration-200",
|
style={{ height: headerHeight }}
|
||||||
{ "bg-background/80 backdrop-blur-sm shadow-sm": isScrolled },
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="w-10">
|
<div className="w-10">
|
||||||
@@ -255,11 +276,15 @@ const ConnectionsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-0 left-0 right-0 bottom-0 pt-28">
|
<div
|
||||||
|
ref={scrollerRefCallback}
|
||||||
|
className="absolute left-0 right-0 bottom-0 overflow-y-auto"
|
||||||
|
style={{ top: headerHeight }}
|
||||||
|
>
|
||||||
{filterConn.length === 0 ? (
|
{filterConn.length === 0 ? (
|
||||||
<BaseEmpty />
|
<BaseEmpty />
|
||||||
) : isTableLayout ? (
|
) : isTableLayout ? (
|
||||||
<div className="p-4 pt-0 h-full w-full">
|
<div className="p-4 pt-0">
|
||||||
<ConnectionTable
|
<ConnectionTable
|
||||||
connections={filterConn}
|
connections={filterConn}
|
||||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [profileItems, activateProfile]);
|
}, [profileItems, activateProfile]);
|
||||||
|
|
||||||
|
|
||||||
const handleProfileChange = useLockFn(async (uid: string) => {
|
const handleProfileChange = useLockFn(async (uid: string) => {
|
||||||
if (profiles?.current === uid) return;
|
if (profiles?.current === uid) return;
|
||||||
await activateProfile(uid, true);
|
await activateProfile(uid, true);
|
||||||
@@ -204,12 +203,47 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-self-center flex flex-col items-center gap-2">
|
<div className="justify-self-center flex flex-col items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative flex items-center justify-center">
|
||||||
{profileItems.length > 0 && (
|
{profileItems.length > 0 ? (
|
||||||
<div className="flex-shrink-0">
|
<>
|
||||||
|
<div className="absolute right-full mr-2">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => viewerRef.current?.create()}
|
||||||
|
className={cn(
|
||||||
|
"backdrop-blur-sm bg-white/80 border-gray-300/60",
|
||||||
|
"dark:bg-white/5 dark:border-white/15",
|
||||||
|
"hover:bg-white/90 hover:border-gray-400/70",
|
||||||
|
"dark:hover:bg-white/10 dark:hover:border-white/20",
|
||||||
|
"transition-all duration-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PlusCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("Add Profile")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" className="w-full max-w-[250px] sm:max-w-xs">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full max-w-[250px] sm:max-w-xs",
|
||||||
|
"backdrop-blur-sm bg-white/80 border-gray-300/60",
|
||||||
|
"dark:bg-white/5 dark:border-white/15",
|
||||||
|
"hover:bg-white/90 hover:border-gray-400/70",
|
||||||
|
"dark:hover:bg-white/10 dark:hover:border-white/20",
|
||||||
|
"transition-all duration-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<span className="truncate">{currentProfileName}</span>
|
<span className="truncate">{currentProfileName}</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -228,17 +262,10 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onSelect={() => viewerRef.current?.create()}>
|
|
||||||
<PlusCircle className="mr-2 h-4 w-4" />
|
|
||||||
<span>{t("Add Profile")}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{currentProfile?.type === 'remote' && (
|
{currentProfile?.type === 'remote' && (
|
||||||
<div className="absolute top-1/2 -translate-y-1/2 left-full ml-2">
|
<div className="absolute left-full ml-2">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -247,7 +274,14 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleUpdateProfile}
|
onClick={handleUpdateProfile}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
className="flex-shrink-0"
|
className={cn(
|
||||||
|
"flex-shrink-0",
|
||||||
|
"backdrop-blur-sm bg-white/70 border border-gray-300/50",
|
||||||
|
"dark:bg-white/5 dark:border-white/10",
|
||||||
|
"hover:bg-white/85 hover:border-gray-400/60",
|
||||||
|
"dark:hover:bg-white/10 dark:hover:border-white/15",
|
||||||
|
"transition-all duration-200"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isUpdating ? <Loader2 className="h-5 w-5 animate-spin" /> : <RefreshCw className="h-5 w-5" />}
|
{isUpdating ? <Loader2 className="h-5 w-5 animate-spin" /> : <RefreshCw className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -257,6 +291,48 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="absolute right-full mr-2">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => viewerRef.current?.create()}
|
||||||
|
className={cn(
|
||||||
|
"backdrop-blur-sm bg-white/80 border-gray-300/60",
|
||||||
|
"dark:bg-white/5 dark:border-white/15",
|
||||||
|
"hover:bg-white/90 hover:border-gray-400/70",
|
||||||
|
"dark:hover:bg-white/10 dark:hover:border-white/20",
|
||||||
|
"transition-all duration-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PlusCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("Add Profile")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled
|
||||||
|
className={cn(
|
||||||
|
"max-w-[250px] sm:max-w-xs opacity-50 cursor-not-allowed",
|
||||||
|
"backdrop-blur-sm bg-white/50 border-gray-300/40",
|
||||||
|
"dark:bg-white/3 dark:border-white/10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{t("No profiles available")}</span>
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-30" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
2
src/services/types.d.ts
vendored
2
src/services/types.d.ts
vendored
@@ -154,7 +154,7 @@ interface IConnectionsItem {
|
|||||||
start: string;
|
start: string;
|
||||||
chains: string[];
|
chains: string[];
|
||||||
rule: string;
|
rule: string;
|
||||||
rulePayload: string;
|
rulePayload?: string;
|
||||||
curUpload?: number; // upload speed, calculate at runtime
|
curUpload?: number; // upload speed, calculate at runtime
|
||||||
curDownload?: number; // download speed, calculate at runtime
|
curDownload?: number; // download speed, calculate at runtime
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user