From 1961a0afc1f299f872d6024757d6498c829c2db7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 13 Dec 2023 20:15:10 -0700 Subject: [PATCH] Support storage page in new ui (#8948) * Add table and oveview components * Add toooltips for cards * Add storage * Undo --- web-new/package-lock.json | 35 ++++ web-new/package.json | 1 + web-new/src/components/ui/table.tsx | 117 +++++++++++++ web-new/src/components/ui/tooltip.tsx | 28 +++ web-new/src/pages/Storage.tsx | 234 ++++++++++++++++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 web-new/src/components/ui/table.tsx create mode 100644 web-new/src/components/ui/tooltip.tsx diff --git a/web-new/package-lock.json b/web-new/package-lock.json index 9e594ef17..52127da3e 100644 --- a/web-new/package-lock.json +++ b/web-new/package-lock.json @@ -23,6 +23,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", "axios": "^1.6.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -1628,6 +1629,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", diff --git a/web-new/package.json b/web-new/package.json index 794fcc863..c085ad431 100644 --- a/web-new/package.json +++ b/web-new/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", "axios": "^1.6.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", diff --git a/web-new/src/components/ui/table.tsx b/web-new/src/components/ui/table.tsx new file mode 100644 index 000000000..7f3502f8b --- /dev/null +++ b/web-new/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/web-new/src/components/ui/tooltip.tsx b/web-new/src/components/ui/tooltip.tsx new file mode 100644 index 000000000..e121f0aea --- /dev/null +++ b/web-new/src/components/ui/tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/web-new/src/pages/Storage.tsx b/web-new/src/pages/Storage.tsx index 00886c004..a37c8983d 100644 --- a/web-new/src/pages/Storage.tsx +++ b/web-new/src/pages/Storage.tsx @@ -1,9 +1,243 @@ +import { useWs } from "@/api/ws"; +import ActivityIndicator from "@/components/ui/activity-indicator"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import Heading from "@/components/ui/heading"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useMemo } from "react"; +import { LuAlertCircle } from "react-icons/lu"; +import useSWR from "swr"; + +type CameraStorage = { + [key: string]: { + bandwidth: number; + usage: number; + usage_percent: number; + }; +}; + +const emptyObject = Object.freeze({}); function Storage() { + const { data: storage } = useSWR("recordings/storage"); + + const { + value: { payload: stats }, + } = useWs("stats", ""); + const { data: initialStats } = useSWR("stats"); + + const { service } = stats || initialStats || emptyObject; + + const hasSeparateMedia = useMemo(() => { + return ( + service && + service["storage"]["/media/frigate/recordings"]["total"] != + service["storage"]["/media/frigate/clips"]["total"] + ); + }, service); + + const getUnitSize = (MB: number) => { + if (isNaN(MB) || MB < 0) return "Invalid number"; + if (MB < 1024) return `${MB} MiB`; + if (MB < 1048576) return `${(MB / 1024).toFixed(2)} GiB`; + + return `${(MB / 1048576).toFixed(2)} TiB`; + }; + + if (!service || !storage) { + return ; + } + return ( <> Storage + + + Overview + +
+ + +
+ Data + + + + + + +

+ Overview of total used storage and total capacity of the + drives that hold the recordings and snapshots directories. +

+
+
+
+
+
+ + + + + Location + Used + Total + + + + + + {hasSeparateMedia ? "Recordings" : "Recordings & Snapshots"} + + + {getUnitSize( + service["storage"]["/media/frigate/recordings"]["used"] + )} + + + {getUnitSize( + service["storage"]["/media/frigate/recordings"]["total"] + )} + + + {hasSeparateMedia && ( + + Snapshots + + {getUnitSize( + service["storage"]["/media/frigate/clips"]["used"] + )} + + + {getUnitSize( + service["storage"]["/media/frigate/clips"]["total"] + )} + + + )} + +
+
+
+ + + +
+ Memory + + + + + + +

Overview of used and total memory in frigate process.

+
+
+
+
+
+ + + + + Location + Used + Total + + + + + /dev/shm + + {getUnitSize(service["storage"]["/dev/shm"]["used"])} + + + {getUnitSize(service["storage"]["/dev/shm"]["total"])} + + + + /tmp/cache + + {getUnitSize(service["storage"]["/tmp/cache"]["used"])} + + + {getUnitSize(service["storage"]["/tmp/cache"]["total"])} + + + +
+
+
+
+ +
+ Cameras + + + + + + +

Overview of per-camera storage usage and bandwidth.

+
+
+
+
+ +
+ {Object.entries(storage).map(([name, camera]) => ( + + +
+ + + + Usage + Stream Bandwidth + + + + + + {Math.round(camera["usage_percent"] ?? 0)}% + + + {camera["bandwidth"] + ? `${getUnitSize(camera["bandwidth"])}/hr` + : "Calculating..."} + + + +
+
+
+ ))} +
); }