diff --git a/package-lock.json b/package-lock.json index 844e7aa..d4f85e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-visually-hidden": "^1.1.1", "@tauri-apps/api": "^2.1.1", "@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-store": "^2.1.0", @@ -1507,6 +1509,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz", + "integrity": "sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz", @@ -1550,6 +1583,29 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", + "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slider": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz", @@ -1748,12 +1804,50 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", - "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", + "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.0" + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1770,6 +1864,24 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", diff --git a/package.json b/package.json index c548397..6e438ee 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,12 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-visually-hidden": "^1.1.1", "@tauri-apps/api": "^2.1.1", "@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-store": "^2.1.0", diff --git a/src/components/charts/DoughnutChart.tsx b/src/components/charts/DoughnutChart.tsx index 7b30e5f..330f141 100644 --- a/src/components/charts/DoughnutChart.tsx +++ b/src/components/charts/DoughnutChart.tsx @@ -1,94 +1,125 @@ import { useSettingsAtom } from "@/atom/useSettingsAtom"; -import { displayHardType } from "@/consts/chart"; -import type { ChartDataType, HardwareDataType } from "@/types/hardwareDataType"; +import { type ChartConfig, ChartContainer } from "@/components/ui/chart"; +import { Skeleton } from "@/components/ui/skeleton"; +import type { HardwareDataType } from "@/types/hardwareDataType"; import { Lightning, Speedometer, Thermometer } from "@phosphor-icons/react"; -import { - ArcElement, - Chart as ChartJS, - type ChartOptions, - Legend, - Tooltip, -} from "chart.js"; -import { Doughnut } from "react-chartjs-2"; import { useTranslation } from "react-i18next"; +import { + Label, + PolarGrid, + PolarRadiusAxis, + RadialBar, + RadialBarChart, +} from "recharts"; -ChartJS.register(ArcElement, Tooltip, Legend); - -const DoughnutChart = ({ - chartData, - hardType, +export const DoughnutChart = ({ + chartValue, dataType, - showTitle, }: { - chartData: number; - hardType: ChartDataType; + chartValue: number; dataType: HardwareDataType; - showTitle: boolean; }) => { - const { settings } = useSettingsAtom(); - const { t } = useTranslation(); + const { settings } = useSettingsAtom(); - const displayDataType: Record = { - usage: t("shared.usage"), - temp: t("shared.temperature"), - clock: t("shared.clock"), - }; - - const data = { - datasets: [ - { - data: [chartData, 100 - chartData], - backgroundColor: { - light: ["#374151", "#f3f4f6"], - dark: ["#888", "#222"], - }[settings.theme], - borderWidth: 0, - }, - ], - }; - - const options: ChartOptions<"doughnut"> = { - cutout: "85%", - plugins: { - tooltip: { enabled: false }, + const chartConfig: Record< + HardwareDataType, + { label: string; color: string } + > = { + usage: { + label: t("shared.usage"), + color: "hsl(var(--chart-2))", }, - }; + temp: { + label: t("shared.temperature"), + color: "hsl(var(--chart-3))", + }, + clock: { + label: t("shared.clock"), + color: "hsl(var(--chart-4))", + }, + } satisfies ChartConfig; + + const chartData = [ + { type: dataType, value: chartValue, fill: `var(--color-${dataType})` }, + ]; const dataTypeIcons: Record = { - usage: , - temp: , - clock: , + usage: , + temp: , + clock: , }; const dataTypeUnits: Record = { usage: "%", - temp: "℃", + temp: "°C", clock: "MHz", }; return ( -
-

- { - showTitle - ? displayHardType[hardType] - : " " /** [TODO] タイトルはコンポーネント外のほうが使いやすそう */ - } -

- -
- - {chartData} - {dataTypeUnits[dataType]} - -
- - {dataTypeIcons[dataType]} - {displayDataType[dataType]} - -
+ + {chartData[0].value != null ? ( + + + + + + + ) : ( +
+ +
+ )} +
); }; - -export default DoughnutChart; diff --git a/src/components/charts/ProcessTable.tsx b/src/components/charts/ProcessTable.tsx index 384561b..fb86c45 100644 --- a/src/components/charts/ProcessTable.tsx +++ b/src/components/charts/ProcessTable.tsx @@ -1,25 +1,36 @@ +import { useSettingsAtom } from "@/atom/useSettingsAtom"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { useTauriDialog } from "@/hooks/useTauriDialog"; import { type ProcessInfo, commands } from "@/rspc/bindings"; -import { CaretDown } from "@phosphor-icons/react"; +import { ArrowsOut, CaretDown, CaretUp } from "@phosphor-icons/react"; +import { DialogDescription } from "@radix-ui/react-dialog"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { atom, useAtom, useSetAtom } from "jotai"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { twMerge } from "tailwind-merge"; +import { ScrollArea, ScrollBar } from "../ui/scroll-area"; const processesAtom = atom([]); -const ProcessesTable = ({ - defaultItemLength, -}: { defaultItemLength: number }) => { +export const ProcessesTable = () => { const { t } = useTranslation(); + const { settings } = useSettingsAtom(); const { error } = useTauriDialog(); const [processes] = useAtom(processesAtom); const setAtom = useSetAtom(processesAtom); - const [showAllItem, setShowAllItem] = useState(false); const [sortConfig, setSortConfig] = useState<{ key: keyof ProcessInfo; direction: "ascending" | "descending"; } | null>(null); + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { const fetchProcesses = async () => { try { @@ -36,7 +47,7 @@ const ProcessesTable = ({ const interval = setInterval(fetchProcesses, 3000); return () => clearInterval(interval); - }, [setAtom, error]); + }, []); const sortedProcesses = [...processes]; if (sortConfig !== null) { @@ -87,67 +98,141 @@ const ProcessesTable = ({ }; return ( -
-

{t("shared.process")}

-
- - - - - - - - - - - {sortedProcesses - .slice(0, showAllItem ? processes.length : defaultItemLength) - .map((process) => ( - - - - - - - ))} - -
requestSort("pid")} - onKeyDown={() => requestSort("pid")} - > - PID - requestSort("name")} - onKeyDown={() => requestSort("name")} - > - {t("shared.name")} - requestSort("cpuUsage")} - onKeyDown={() => requestSort("cpuUsage")} - > - {t("shared.cpuUsage")} - requestSort("memoryUsage")} - onKeyDown={() => requestSort("memoryUsage")} - > - {t("shared.memoryUsage")} -
{process.pid}{process.name}{process.cpuUsage}%{process.memoryUsage} MB
- {!showAllItem && ( - - )} -
+
+ +
+

{t("shared.process")}

+
+ + + +
+
+ +
+ +
+ + + + {t("shared.process")} + + Expand the process as a list + + + + +
); }; -export default ProcessesTable; +const InfoTable = ({ + processes, + sortConfig, + requestSort, + className, +}: { + processes: ProcessInfo[]; + requestSort: (key: keyof ProcessInfo) => void; + sortConfig: { + key: keyof ProcessInfo; + direction: "ascending" | "descending"; + } | null; + className: string; +}) => { + const { t } = useTranslation(); + + const sortIcon: Record<"ascending" | "descending", JSX.Element> = { + ascending: , + descending: , + }; + + return ( + + + + + + + + + + + + {processes.map((process) => ( + + + + + + + ))} + +
requestSort("pid")} + onKeyDown={() => requestSort("pid")} + > +
+ PID + {sortConfig && + sortConfig.key === "pid" && + sortIcon[sortConfig.direction]} +
+
requestSort("name")} + onKeyDown={() => requestSort("name")} + > +
+ {t("shared.name")} + {sortConfig && + sortConfig.key === "name" && + sortIcon[sortConfig.direction]} +
+
requestSort("cpuUsage")} + onKeyDown={() => requestSort("cpuUsage")} + > +
+ {t("shared.cpuUsage")} + {sortConfig && + sortConfig.key === "cpuUsage" && + sortIcon[sortConfig.direction]} +
+
requestSort("memoryUsage")} + onKeyDown={() => requestSort("memoryUsage")} + > +
+ {t("shared.memoryUsage")} + {sortConfig && + sortConfig.key === "memoryUsage" && + sortIcon[sortConfig.direction]} +
+
{process.pid}{process.name}{process.cpuUsage}%{process.memoryUsage} MB
+ +
+ ); +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..8f935b7 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..f551e31 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..1492c99 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,18 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/src/template/Dashboard.tsx b/src/template/Dashboard.tsx index 7596965..fd7d731 100644 --- a/src/template/Dashboard.tsx +++ b/src/template/Dashboard.tsx @@ -1,17 +1,17 @@ import { cpuUsageHistoryAtom, - gpuFanSpeedAtom, gpuTempAtom, graphicUsageHistoryAtom, memoryUsageHistoryAtom, } from "@/atom/chart"; import { useHardwareInfoAtom } from "@/atom/useHardwareInfoAtom"; +import { useSettingsAtom } from "@/atom/useSettingsAtom"; import { StorageBarChart, type StorageBarChartData, } from "@/components/charts/Bar"; -import DoughnutChart from "@/components/charts/DoughnutChart"; -import ProcessesTable from "@/components/charts/ProcessTable"; +import { DoughnutChart } from "@/components/charts/DoughnutChart"; +import { ProcessesTable } from "@/components/charts/ProcessTable"; import type { StorageInfo } from "@/rspc/bindings"; import type { NameValues } from "@/types/hardwareDataType"; import { useAtom } from "jotai"; @@ -19,8 +19,15 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; const InfoTable = ({ data }: { data: { [key: string]: string | number } }) => { + const { settings } = useSettingsAtom(); + return ( -
+
{Object.keys(data).map((key) => ( @@ -35,12 +42,23 @@ const InfoTable = ({ data }: { data: { [key: string]: string | number } }) => { ); }; -const DataArea = ({ children }: { children: React.ReactNode }) => { +const DataArea = ({ + children, + title, + border = true, +}: { children: React.ReactNode; title?: string; border?: boolean }) => { return (
-
-
{children}
-
+ {border ? ( +
+ {title && ( +

{title}

+ )} +
{children}
+
+ ) : ( + <>{children} + )}
); }; @@ -54,10 +72,8 @@ const CPUInfo = () => { hardwareInfo.cpu && ( <> { const { t } = useTranslation(); const [graphicUsageHistory] = useAtom(graphicUsageHistoryAtom); const [gpuTemp] = useAtom(gpuTempAtom); - const [gpuFan] = useAtom(gpuFanSpeedAtom); const { hardwareInfo } = useHardwareInfoAtom(); const getTargetInfo = (data: NameValues) => { @@ -87,33 +102,17 @@ const GPUInfo = () => { }; const targetTemperature = getTargetInfo(gpuTemp); - const targetFanSpeed = getTargetInfo(gpuFan); return ( hardwareInfo.gpus && ( <> -
+
{targetTemperature && ( - - )} - {targetFanSpeed && ( - + )}
@@ -143,10 +142,8 @@ const MemoryInfo = () => { hardwareInfo.memory && ( <> { ); return ( -
-

{t("shared.storage")}

+
{sortedStorage.map((storage) => { @@ -225,20 +221,16 @@ const StorageDataInfo = () => { const Dashboard = () => { const { hardwareInfo } = useHardwareInfoAtom(); const { init } = useHardwareInfoAtom(); + const { t } = useTranslation(); // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { init(); }, []); - const hardwareInfoList: { key: string; component: JSX.Element }[] = [ + const hardwareInfoListLeft: { key: string; component: JSX.Element }[] = [ hardwareInfo.cpu && { key: "cpuInfo", component: }, - hardwareInfo.gpus && { key: "gpuInfo", component: }, hardwareInfo.memory && { key: "memoryInfo", component: }, - { - key: "processesTable", - component: , - }, hardwareInfo.storage && hardwareInfo.storage.length > 0 ? { key: "storageInfo", @@ -247,11 +239,41 @@ const Dashboard = () => { : undefined, ].filter((x) => x != null); + const hardwareInfoListRight: { key: string; component: JSX.Element }[] = [ + hardwareInfo.gpus && { key: "gpuInfo", component: }, + { + key: "processesTable", + component: , + }, + ].filter((x) => x != null); + + const dataAreaKey2Title: Record = { + cpuInfo: "CPU", + memoryInfo: "RAM", + storageInfo: t("shared.storage"), + gpuInfo: "GPU", + }; + return ( -
- {hardwareInfoList.map(({ key, component }) => ( - {component} - ))} +
+
+ {hardwareInfoListLeft.map(({ key, component }) => ( + + {component} + + ))} +
+
+ {hardwareInfoListRight.map(({ key, component }) => ( + + {component} + + ))} +
); };