From 4f08ef5a1b4b44c8bdd054a3f073cc2cc924d003 Mon Sep 17 00:00:00 2001 From: Manuel Date: Sun, 21 Jul 2024 16:24:58 +0200 Subject: [PATCH] feat: add hardware usage widget --- .../api/src/router/widgets/hardware-usage.ts | 53 ++ packages/api/src/router/widgets/index.ts | 2 + packages/cron-jobs-core/src/creator.ts | 2 +- packages/cron-jobs-core/src/expressions.ts | 1 + packages/cron-jobs/src/index.ts | 2 + .../src/jobs/integrations/hardware-usage.ts | 67 +++ packages/definitions/src/integration.ts | 11 +- packages/definitions/src/widget.ts | 1 + packages/integrations/src/base/creator.ts | 2 + packages/integrations/src/base/integration.ts | 8 + .../src/dashdot/dashdot-integration.ts | 93 ++++ packages/integrations/src/index.ts | 7 +- .../src/interfaces/hardware-usage/cpu-load.ts | 3 + .../interfaces/hardware-usage/memory-load.ts | 3 + .../interfaces/hardware-usage/network-load.ts | 4 + .../interfaces/hardware-usage/server-info.ts | 3 + .../src/widgets/definitions/index.ts | 1 + packages/old-import/src/widgets/options.ts | 1 + packages/widgets/package.json | 3 + .../widgets/src/hardware-usage/component.tsx | 62 +++ .../src/hardware-usage/graphs/cpu-graph.tsx | 71 +++ .../hardware-usage/graphs/memory-graph.tsx | 73 +++ .../hardware-usage/graphs/network-graph.tsx | 104 ++++ .../src/hardware-usage/graphs/wrapper.tsx | 22 + packages/widgets/src/hardware-usage/index.ts | 10 + packages/widgets/src/index.tsx | 2 + pnpm-lock.yaml | 457 +++++++++++++++++- 27 files changed, 1059 insertions(+), 9 deletions(-) create mode 100644 packages/api/src/router/widgets/hardware-usage.ts create mode 100644 packages/cron-jobs/src/jobs/integrations/hardware-usage.ts create mode 100644 packages/integrations/src/dashdot/dashdot-integration.ts create mode 100644 packages/integrations/src/interfaces/hardware-usage/cpu-load.ts create mode 100644 packages/integrations/src/interfaces/hardware-usage/memory-load.ts create mode 100644 packages/integrations/src/interfaces/hardware-usage/network-load.ts create mode 100644 packages/integrations/src/interfaces/hardware-usage/server-info.ts create mode 100644 packages/widgets/src/hardware-usage/component.tsx create mode 100644 packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx create mode 100644 packages/widgets/src/hardware-usage/graphs/memory-graph.tsx create mode 100644 packages/widgets/src/hardware-usage/graphs/network-graph.tsx create mode 100644 packages/widgets/src/hardware-usage/graphs/wrapper.tsx create mode 100644 packages/widgets/src/hardware-usage/index.ts diff --git a/packages/api/src/router/widgets/hardware-usage.ts b/packages/api/src/router/widgets/hardware-usage.ts new file mode 100644 index 000000000..a2f38b585 --- /dev/null +++ b/packages/api/src/router/widgets/hardware-usage.ts @@ -0,0 +1,53 @@ +import { observable } from "@trpc/server/observable"; + +import type { CpuLoad, MemoryLoad, NetworkLoad, ServerInfo } from "@homarr/integrations"; +import { createItemAndIntegrationChannel } from "@homarr/redis"; + +import { createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const hardwareUsageRouter = createTRPCRouter({ + getServerInfo: publicProcedure + .unstable_concat(createOneIntegrationMiddleware("query", "getDashDot")) + .query(async ({ ctx }) => { + const channel = createItemAndIntegrationChannel<{ + info: ServerInfo; + }>("hardwareUsage", ctx.integration.id); + const data = await channel.getAsync(); + return { + info: data?.data.info ?? ({} as ServerInfo), + }; + }), + getHardwareInformationHistory: publicProcedure + .unstable_concat(createOneIntegrationMiddleware("query", "getDashDot")) + .query(async ({ ctx }) => { + const channel = createItemAndIntegrationChannel<{ + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>("hardwareUsage", ctx.integration.id); + const data = await channel.getAsync(); + return { + cpuLoad: data?.data.cpuLoad ?? ({} as CpuLoad), + memoryLoad: data?.data.memoryLoad ?? ({} as MemoryLoad), + networkLoad: data?.data.networkLoad ?? ({} as NetworkLoad), + }; + }), + subscribeCpu: publicProcedure + .unstable_concat(createOneIntegrationMiddleware("query", "getDashDot")) + .subscription(({ ctx }) => { + return observable<{ cpuLoad: CpuLoad; memoryLoad: MemoryLoad; networkLoad: NetworkLoad }>((emit) => { + const channel = createItemAndIntegrationChannel<{ + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>("hardwareUsage", ctx.integration.id); + const unsubscribe = channel.subscribe((data) => { + emit.next(data); + }); + return () => { + unsubscribe(); + }; + }); + }), +}); diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 7f030a4bb..a475e9c12 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -3,6 +3,7 @@ import { appRouter } from "./app"; import { calendarRouter } from "./calendar"; import { dnsHoleRouter } from "./dns-hole"; import { downloadsRouter } from "./downloads"; +import { hardwareUsageRouter } from "./hardware-usage"; import { healthMonitoringRouter } from "./health-monitoring"; import { indexerManagerRouter } from "./indexer-manager"; import { mediaRequestsRouter } from "./media-requests"; @@ -20,6 +21,7 @@ export const widgetRouter = createTRPCRouter({ smartHome: smartHomeRouter, mediaServer: mediaServerRouter, calendar: calendarRouter, + hardwareUsage: hardwareUsageRouter, downloads: downloadsRouter, mediaRequests: mediaRequestsRouter, rssFeed: rssFeedRouter, diff --git a/packages/cron-jobs-core/src/creator.ts b/packages/cron-jobs-core/src/creator.ts index a25535db6..7492256b9 100644 --- a/packages/cron-jobs-core/src/creator.ts +++ b/packages/cron-jobs-core/src/creator.ts @@ -34,7 +34,7 @@ const createCallback = { + const itemsForIntegration = await db.query.items.findMany({ + where: eq(items.kind, "hardwareUsage"), + with: { + integrations: { + with: { + integration: { + with: { + secrets: { + columns: { + kind: true, + value: true, + }, + }, + }, + }, + }, + }, + }, + }); + + for (const itemForIntegration of itemsForIntegration) { + for (const integration of itemForIntegration.integrations) { + const dashDotIntegration = new DashDotIntegration({ + ...integration.integration, + decryptedSecrets: integration.integration.secrets.map((secret) => ({ + ...secret, + value: decryptSecret(secret.value), + })), + }); + + const info = await dashDotIntegration.getInfoAsync(); + const cpuLoad = await dashDotIntegration.getCurrentCpuLoadAsync(); + const memoryLoad = await dashDotIntegration.getCurrentMemoryLoadAsync(); + const networkLoad = await dashDotIntegration.getCurrentNetworkLoadAsync(); + + const cache = createItemAndIntegrationChannel<{ + info: ServerInfo; + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>("hardwareUsage", integration.integrationId); + await cache.setAsync({ + memoryLoad, + networkLoad, + cpuLoad, + info, + }); + await cache.publishAndUpdateLastStateAsync({ + cpuLoad, + networkLoad, + memoryLoad, + info, + }); + } + } +}); diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index f9f6cc3e7..6bb56a05e 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -12,7 +12,7 @@ export const integrationSecretKinds = objectKeys(integrationSecretKindObject); interface integrationDefinition { name: string; iconUrl: string; - secretKinds: AtLeastOneOf; // at least one secret kind set is required + secretKinds: IntegrationSecretKind[][]; category: AtLeastOneOf; supportsSearch: boolean; } @@ -137,6 +137,12 @@ export const integrationDefs = { category: ["smartHomeServer"], supportsSearch: false, }, + getDashDot: { + name: "Dash.", + secretKinds: [[]], + category: ["hardware"], + iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/dashdot.png", + }, openmediavault: { name: "OpenMediaVault", secretKinds: [["username", "password"]], @@ -210,4 +216,5 @@ export type IntegrationCategory = | "torrent" | "smartHomeServer" | "indexerManager" - | "healthMonitoring"; + | "healthMonitoring" + | "hardware"; diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 0f68c6577..16046179d 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -11,6 +11,7 @@ export const widgetKinds = [ "smartHome-executeAutomation", "mediaServer", "calendar", + "hardwareUsage", "downloads", "mediaRequests-requestList", "mediaRequests-requestStats", diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index 4c6e2aa6d..5d949c83b 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -4,6 +4,7 @@ import type { Integration as DbIntegration } from "@homarr/db/schema/sqlite"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration"; +import { DashDotIntegration } from "../dashdot/dashdot-integration"; import { DelugeIntegration } from "../download-client/deluge/deluge-integration"; import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration"; import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration"; @@ -65,6 +66,7 @@ export const integrationCreators = { jellyseerr: JellyseerrIntegration, overseerr: OverseerrIntegration, prowlarr: ProwlarrIntegration, + getDashDot: DashDotIntegration, openmediavault: OpenMediaVaultIntegration, lidarr: LidarrIntegration, readarr: ReadarrIntegration, diff --git a/packages/integrations/src/base/integration.ts b/packages/integrations/src/base/integration.ts index 69b346e23..69e000f94 100644 --- a/packages/integrations/src/base/integration.ts +++ b/packages/integrations/src/base/integration.ts @@ -29,6 +29,14 @@ export abstract class Integration { return secret.value; } + protected appendPathToUrlWithEndingSlash(basename: string, path: string) { + if (basename.endsWith("/")) { + return `${basename}${path}`; + } + + return `${basename}/${path}`; + } + /** * Test the connection to the integration * @throws {IntegrationTestConnectionError} if the connection fails diff --git a/packages/integrations/src/dashdot/dashdot-integration.ts b/packages/integrations/src/dashdot/dashdot-integration.ts new file mode 100644 index 000000000..8d28fa8c2 --- /dev/null +++ b/packages/integrations/src/dashdot/dashdot-integration.ts @@ -0,0 +1,93 @@ +import { Integration } from "../base/integration"; +import type { CpuLoad } from "../interfaces/hardware-usage/cpu-load"; +import type { MemoryLoad } from "../interfaces/hardware-usage/memory-load"; +import type { NetworkLoad } from "../interfaces/hardware-usage/network-load"; +import type { ServerInfo } from "../interfaces/hardware-usage/server-info"; + +export class DashDotIntegration extends Integration { + public async testConnectionAsync(): Promise { + const response = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "info")); + await response.json(); + } + + public async getInfoAsync(): Promise { + const infoResponse = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "info")); + const serverInfo = (await infoResponse.json()) as InternalServerInfo; + return { + maxAvailableMemoryBytes: serverInfo.ram.size, + }; + } + + public async getCurrentCpuLoadAsync(): Promise { + const cpu = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "load/cpu")); + const data = (await cpu.json()) as CpuLoadApi[]; + return { + sumLoad: data.reduce((acc, current) => acc + current.load, 0) / data.length, + }; + } + + public async getCurrentMemoryLoadAsync(): Promise { + const memoryLoad = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "load/ram")); + const data = (await memoryLoad.json()) as MemoryLoadApi; + return { + loadInBytes: data.load, + }; + } + + public async getCurrentNetworkLoadAsync(): Promise { + const memoryLoad = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "load/network")); + const data = (await memoryLoad.json()) as NetworkLoadApi; + return { + down: data.down, + up: data.up, + }; + } +} + +/** + * CPU load per core + */ +interface CpuLoadApi { + load: number; +} + +interface MemoryLoadApi { + load: number; +} + +interface NetworkLoadApi { + up: number; + down: number; +} + +interface InternalServerInfo { + ram: { + /** + * Available memory in bytes + */ + size: number; + }; + storage: { + /** + * Size of storage in bytes + */ + size: number; + disks: { + /** + * Name of the device, e.g. sda + */ + device: string; + + /** + * Brand name of the device + */ + brand: string; + + /** + * Type of the device. + * See option "physical" of https://systeminformation.io/filesystem.html + */ + type: string; + }[]; + }[]; +} diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index ea13ce13b..84dbf92c3 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -8,9 +8,10 @@ export { TransmissionIntegration } from "./download-client/transmission/transmis export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration"; export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration"; export { JellyfinIntegration } from "./jellyfin/jellyfin-integration"; +export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; +export { DashDotIntegration } from "./dashdot/dashdot-integration"; export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration"; export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration"; -export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration"; export { OverseerrIntegration } from "./overseerr/overseerr-integration"; export { PiHoleIntegration } from "./pi-hole/pi-hole-integration"; @@ -24,6 +25,10 @@ export type { IntegrationInput } from "./base/integration"; export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data"; export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items"; export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status"; +export type { CpuLoad } from "./interfaces/hardware-usage/cpu-load"; +export type { MemoryLoad } from "./interfaces/hardware-usage/memory-load"; +export type { NetworkLoad } from "./interfaces/hardware-usage/network-load"; +export type { ServerInfo } from "./interfaces/hardware-usage/server-info"; export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring"; export { MediaRequestStatus } from "./interfaces/media-requests/media-request"; export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request"; diff --git a/packages/integrations/src/interfaces/hardware-usage/cpu-load.ts b/packages/integrations/src/interfaces/hardware-usage/cpu-load.ts new file mode 100644 index 000000000..969083c8c --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/cpu-load.ts @@ -0,0 +1,3 @@ +export interface CpuLoad { + sumLoad: number; +} diff --git a/packages/integrations/src/interfaces/hardware-usage/memory-load.ts b/packages/integrations/src/interfaces/hardware-usage/memory-load.ts new file mode 100644 index 000000000..f8e460e3c --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/memory-load.ts @@ -0,0 +1,3 @@ +export interface MemoryLoad { + loadInBytes: number; +} diff --git a/packages/integrations/src/interfaces/hardware-usage/network-load.ts b/packages/integrations/src/interfaces/hardware-usage/network-load.ts new file mode 100644 index 000000000..caf0ebf5f --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/network-load.ts @@ -0,0 +1,4 @@ +export interface NetworkLoad { + up: number; + down: number; +} diff --git a/packages/integrations/src/interfaces/hardware-usage/server-info.ts b/packages/integrations/src/interfaces/hardware-usage/server-info.ts new file mode 100644 index 000000000..b5864b55b --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/server-info.ts @@ -0,0 +1,3 @@ +export interface ServerInfo { + maxAvailableMemoryBytes: number; +} diff --git a/packages/old-import/src/widgets/definitions/index.ts b/packages/old-import/src/widgets/definitions/index.ts index d574e0a37..bb095572a 100644 --- a/packages/old-import/src/widgets/definitions/index.ts +++ b/packages/old-import/src/widgets/definitions/index.ts @@ -68,6 +68,7 @@ export const widgetKindMapping = { indexerManager: "indexer-manager", bookmarks: "bookmark", healthMonitoring: "health-monitoring", + hardwareUsage: "dashdot", } satisfies Record; // Use null for widgets that did not exist in oldmarr // TODO: revert assignment so that only old widgets are needed in the object, diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index a95c8a5d9..a82b9037f 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -130,6 +130,7 @@ const optionMapping: OptionMapping = { fileSystem: (oldOptions) => oldOptions.fileSystem, }, app: null, + hardwareUsage: {}, }; /** diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 81873c308..9f17c1bbb 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -43,6 +43,9 @@ "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "^7.14.1", "@mantine/hooks": "^7.14.1", + "@nivo/bar": "^0.87.0", + "@nivo/core": "^0.87.0", + "@nivo/line": "^0.87.0", "@tabler/icons-react": "^3.22.0", "@tiptap/extension-color": "2.10.2", "@tiptap/extension-highlight": "2.10.2", diff --git a/packages/widgets/src/hardware-usage/component.tsx b/packages/widgets/src/hardware-usage/component.tsx new file mode 100644 index 000000000..ac6037e41 --- /dev/null +++ b/packages/widgets/src/hardware-usage/component.tsx @@ -0,0 +1,62 @@ +import { Stack } from "@mantine/core"; +import { useListState } from "@mantine/hooks"; + +import { clientApi } from "@homarr/api/client"; +import type { CpuLoad, MemoryLoad, NetworkLoad } from "@homarr/integrations"; + +import type { WidgetComponentProps } from "../definition"; +import { NoIntegrationSelectedError } from "../errors"; +import { CpuGraph } from "./graphs/cpu-graph"; +import { MemoryGraph } from "./graphs/memory-graph"; +import { NetworkGraph } from "./graphs/network-graph"; + +export default function HardwareUsageWidget({ integrationIds }: WidgetComponentProps<"hardwareUsage">) { + const [hardwareUsageHistory] = clientApi.widget.hardwareUsage.getHardwareInformationHistory.useSuspenseQuery( + { + integrationId: integrationIds[0] ?? "", + }, + {}, + ); + + const [serverInfo] = clientApi.widget.hardwareUsage.getServerInfo.useSuspenseQuery({ + integrationId: integrationIds[0] ?? "", + }); + + const [hardwareUsage, hardwareUsageHandlers] = useListState<{ + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>([hardwareUsageHistory]); + + clientApi.widget.hardwareUsage.subscribeCpu.useSubscription( + { + integrationId: integrationIds[0] ?? "", + }, + { + onData: (data) => { + hardwareUsageHandlers.append(data); + if (hardwareUsage.length > 15) { + hardwareUsageHandlers.shift(); + } + }, + }, + ); + + if (integrationIds.length != 1) { + throw new NoIntegrationSelectedError(); + } + + const hasLast = hardwareUsage.length > 0; + + return ( + + usage.cpuLoad)} hasLast={hasLast} /> + usage.memoryLoad)} + maxAvailableBytes={serverInfo.info.maxAvailableMemoryBytes} + hasLast={hasLast} + /> + usage.networkLoad)} hasLast={hasLast} /> + + ); +} diff --git a/packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx b/packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx new file mode 100644 index 000000000..73ad2de74 --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx @@ -0,0 +1,71 @@ +import { Paper, Text } from "@mantine/core"; +import { ResponsiveLine } from "@nivo/line"; + +import type { CpuLoad } from "@homarr/integrations"; + +import { GraphWrapper } from "./wrapper"; + +interface CpuGraphProps { + cpuLoad: CpuLoad[]; + hasLast: boolean; +} + +export const CpuGraph = ({ cpuLoad, hasLast }: CpuGraphProps) => { + const data = [ + { + id: "cpuLoad", + color: "red", + data: cpuLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.sumLoad, + })), + }, + ]; + return ( + 0 ? cpuLoad[cpuLoad.length - 1]?.sumLoad.toFixed(2) : 0}%`} + showSubtitle={hasLast} + > + { + if (slice.points.length === 0) { + return null; + } + return ( + + + {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} + {slice.points[0]!.data.yFormatted}% + + + ); + }} + curve={"monotoneX"} + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + axisBottom={null} + axisLeft={null} + enablePoints={false} + enableTouchCrosshair={true} + enableGridX={false} + enableGridY={false} + enableCrosshair={true} + useMesh={true} + animate={false} + /> + + ); +}; diff --git a/packages/widgets/src/hardware-usage/graphs/memory-graph.tsx b/packages/widgets/src/hardware-usage/graphs/memory-graph.tsx new file mode 100644 index 000000000..4db335222 --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/memory-graph.tsx @@ -0,0 +1,73 @@ +import { Paper, Text } from "@mantine/core"; +import { ResponsiveLine } from "@nivo/line"; + +import { humanFileSize } from "@homarr/common"; +import type { MemoryLoad } from "@homarr/integrations"; + +import { GraphWrapper } from "./wrapper"; + +interface MemoryGraphProps { + memoryLoad: MemoryLoad[]; + hasLast: boolean; + maxAvailableBytes: number; +} + +export const MemoryGraph = ({ memoryLoad, hasLast, maxAvailableBytes }: MemoryGraphProps) => { + const data = [ + { + id: "memoryLoad", + color: "red", + data: memoryLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.loadInBytes, + })), + }, + ]; + return ( + 0 ? (memoryLoad[memoryLoad.length - 1]?.loadInBytes ?? 0) : 0)} / ${humanFileSize(maxAvailableBytes)}`} + showSubtitle={hasLast} + > + { + if (slice.points.length === 0) { + return null; + } + return ( + + + {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} + {humanFileSize(slice.points[0]!.data.y.valueOf() as number)} + + + ); + }} + curve={"monotoneX"} + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + axisBottom={null} + axisLeft={null} + enablePoints={false} + enableTouchCrosshair={true} + enableGridX={false} + enableGridY={false} + enableCrosshair={true} + useMesh={true} + animate={false} + /> + + ); +}; diff --git a/packages/widgets/src/hardware-usage/graphs/network-graph.tsx b/packages/widgets/src/hardware-usage/graphs/network-graph.tsx new file mode 100644 index 000000000..948c6311a --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/network-graph.tsx @@ -0,0 +1,104 @@ +import type { ReactNode } from "react"; +import { Group, Paper, Stack, Text } from "@mantine/core"; +import { ResponsiveLine } from "@nivo/line"; +import { IconDownload, IconUpload } from "@tabler/icons-react"; + +import { humanFileSize } from "@homarr/common"; +import type { NetworkLoad } from "@homarr/integrations"; + +import { GraphWrapper } from "./wrapper"; + +interface MemoryGraphProps { + networkLoad: NetworkLoad[]; + hasLast: boolean; +} + +export const NetworkGraph = ({ networkLoad, hasLast }: MemoryGraphProps) => { + const data = [ + { + id: "networkUp", + color: "red", + data: networkLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.up, + })), + }, + { + id: "networkDown", + color: "green", + data: networkLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.down, + })), + }, + ]; + + const lastDatapoint = networkLoad.length > 0 ? networkLoad[networkLoad.length - 1] : undefined; + const subtitle: ReactNode = lastDatapoint ? ( + + + + {humanFileSize(Math.round(lastDatapoint.up))}ps + + + + {humanFileSize(Math.round(lastDatapoint.down))}ps + + + ) : ( + <> + ); + + return ( + + { + if (slice.points.length != 2) { + return null; + } + return ( + + + + + + {humanFileSize(Math.round(slice.points[1]?.data.y.valueOf() as number))} + + + + + + {humanFileSize(Math.round(slice.points[0]?.data.y.valueOf() as number))} + + + + + ); + }} + curve={"monotoneX"} + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + axisBottom={null} + axisLeft={null} + enablePoints={false} + enableTouchCrosshair={true} + enableGridX={false} + enableGridY={false} + enableCrosshair={true} + useMesh={true} + animate={false} + /> + + ); +}; diff --git a/packages/widgets/src/hardware-usage/graphs/wrapper.tsx b/packages/widgets/src/hardware-usage/graphs/wrapper.tsx new file mode 100644 index 000000000..9cdd8921f --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/wrapper.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; +import { Group, Paper, Text } from "@mantine/core"; + +interface GraphWrapperProps { + children: ReactNode; + title: string; + subtitle?: string | ReactNode; + showSubtitle?: boolean; + height?: number; +} + +export const GraphWrapper = ({ showSubtitle, subtitle, title, children, height = 125 }: GraphWrapperProps) => { + return ( + + + {title} + {showSubtitle && {subtitle}} + + {children} + + ); +}; diff --git a/packages/widgets/src/hardware-usage/index.ts b/packages/widgets/src/hardware-usage/index.ts new file mode 100644 index 000000000..8f28528cb --- /dev/null +++ b/packages/widgets/src/hardware-usage/index.ts @@ -0,0 +1,10 @@ +import { IconVideo } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +export const { componentLoader, definition } = createWidgetDefinition("hardwareUsage", { + icon: IconVideo, + supportedIntegrations: ["getDashDot"], + options: optionsBuilder.from(() => ({})), +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index e2d83563a..f638fd3d7 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -14,6 +14,7 @@ import type { WidgetComponentProps } from "./definition"; import * as dnsHoleControls from "./dns-hole/controls"; import * as dnsHoleSummary from "./dns-hole/summary"; import * as downloads from "./downloads"; +import * as hardwareUsage from "./hardware-usage"; import * as healthMonitoring from "./health-monitoring"; import * as iframe from "./iframe"; import type { WidgetImportRecord } from "./import"; @@ -45,6 +46,7 @@ export const widgetImports = { "smartHome-executeAutomation": smartHomeExecuteAutomation, mediaServer, calendar, + hardwareUsage, downloads, "mediaRequests-requestList": mediaRequestsList, "mediaRequests-requestStats": mediaRequestsStats, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb2c5e3ec..66902a6b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -871,10 +871,10 @@ importers: version: 0.28.1 drizzle-orm: specifier: ^0.36.4 - version: 0.36.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1) + version: 0.36.4(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1) drizzle-zod: specifier: ^0.5.1 - version: 0.5.1(drizzle-orm@0.36.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1))(zod@3.23.8) + version: 0.5.1(drizzle-orm@0.36.4(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1))(zod@3.23.8) mysql2: specifier: 3.11.4 version: 3.11.4 @@ -1647,6 +1647,15 @@ importers: '@mantine/hooks': specifier: ^7.14.1 version: 7.14.1(react@18.3.1) + '@nivo/bar': + specifier: ^0.87.0 + version: 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': + specifier: ^0.87.0 + version: 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/line': + specifier: ^0.87.0 + version: 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tabler/icons-react': specifier: ^3.22.0 version: 3.22.0(react@18.3.1) @@ -3077,6 +3086,54 @@ packages: cpu: [x64] os: [win32] + '@nivo/annotations@0.87.0': + resolution: {integrity: sha512-4Xk/soEmi706iOKszjX1EcGLBNIvhMifCYXOuLIFlMAXqhw1x2YS7PxickVSskdSzJCwJX4NgQ/R/9u6nxc5OA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/axes@0.87.0': + resolution: {integrity: sha512-zCRBfiRKJi+xOxwxH5Pxq/8+yv3fAYDl4a1F2Ssnp5gMIobwzVsdearvsm5B04e9bfy3ZXTL7KgbkEkSAwu6SA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/bar@0.87.0': + resolution: {integrity: sha512-r/MEVCNAHKfmsy1Fb+JztVczOhIEtAx4VFs2XUbn9YpEDgxydavUJyfoy5/nGq6h5jG1/t47cfB4nZle7c0fyQ==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/colors@0.87.0': + resolution: {integrity: sha512-S4pZzRGKK23t8XAjQMhML6wwsfKO9nH03xuyN4SvCodNA/Dmdys9xV+9Dg/VILTzvzsBTBGTX0dFBg65WoKfVg==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/core@0.87.0': + resolution: {integrity: sha512-yEQWJn7QjWnbmCZccBCo4dligNyNyz3kgyV9vEtcaB1iGeKhg55RJEAlCOul+IDgSCSPFci2SxTmipE6LZEZCg==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/legends@0.87.0': + resolution: {integrity: sha512-bVJCeqEmK4qHrxNaPU/+hXUd/yaKlcQ0yrsR18ewoknVX+pgvbe/+tRKJ+835JXlvRijYIuqwK1sUJQIxyB7oA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/line@0.87.0': + resolution: {integrity: sha512-Ki/WDd8ZU8VWScW4ZeKUFCXRdAEg8nrS+F+jdfJDPxyxUMHZJCAbrXrnsExcEQLOaDQ2aU/bijEMiDS8/dJzuA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/scales@0.87.0': + resolution: {integrity: sha512-IHdY9w2em/xpWurcbhUR3cUA1dgbY06rU8gmA/skFCwf3C4Da3Rqwr0XqvxmkDC+EdT/iFljMbLst7VYiCnSdw==} + + '@nivo/tooltip@0.87.0': + resolution: {integrity: sha512-nZJWyRIt/45V/JBdJ9ksmNm1LFfj59G1Dy9wB63Icf2YwyBT+J+zCzOGXaY7gxCxgF1mnSL3dC7fttcEdXyN/g==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/voronoi@0.87.0': + resolution: {integrity: sha512-Tg+9YnCX8LKsEwZMY1Q83mWiVFiyU2smxrO3JaC9vzjIh/2A/bkNPwC6BdmRaQMvY1jngKs+WKDnNxSQWFSOEg==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + '@noble/hashes@1.5.0': resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} engines: {node: ^14.21.3 || >=16} @@ -3197,6 +3254,42 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@prisma/client@5.16.1': + resolution: {integrity: sha512-wM9SKQjF0qLxdnOZIVAIMKiz6Hu7vDt4FFAih85K1dk/Rr2mdahy6d3QP41K62N9O0DJJA//gUDA3Mp49xsKIg==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + + '@react-spring/animated@9.7.4': + resolution: {integrity: sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.7.4': + resolution: {integrity: sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.7.4': + resolution: {integrity: sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==} + + '@react-spring/shared@9.7.4': + resolution: {integrity: sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/types@9.7.4': + resolution: {integrity: sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==} + + '@react-spring/web@9.7.4': + resolution: {integrity: sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -3769,6 +3862,39 @@ packages: '@types/css-modules@1.0.5': resolution: {integrity: sha512-oeKafs/df9lwOvtfiXVliZsocFVOexK9PZtLQWuPeuVCFR7jwiqlg60lu80JTe5NFNtH3tnV6Fs/ySR8BUPHAw==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-format@1.4.5': + resolution: {integrity: sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale-chromatic@3.0.3': + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time-format@2.3.4': + resolution: {integrity: sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==} + + '@types/d3-time-format@3.0.4': + resolution: {integrity: sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==} + + '@types/d3-time@1.1.4': + resolution: {integrity: sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} @@ -4717,6 +4843,57 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-format@1.4.5: + resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@3.0.0: + resolution: {integrity: sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==} + + d3-time@1.1.0: + resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} + + d3-time@2.1.1: + resolution: {integrity: sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -4804,6 +4981,9 @@ packages: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} engines: {node: '>=8'} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -5727,6 +5907,13 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + intl-messageformat@10.7.1: resolution: {integrity: sha512-xQuJW2WcyzNJZWUu5xTVPOmNSA1Sowuu/NKFdUid5Fxx/Yl6/s4DefTU/y7zy+irZLDmFGmTLtnM8FqpN05wlA==} @@ -7199,6 +7386,9 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.21.3: resolution: {integrity: sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9476,6 +9666,138 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.18': optional: true + '@nivo/annotations@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lodash: 4.17.21 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/axes@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/scales': 0.87.0 + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-format': 1.4.5 + '@types/d3-time-format': 2.3.4 + d3-format: 1.4.5 + d3-time-format: 3.0.0 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/bar@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/annotations': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/axes': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/legends': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/scales': 0.87.0 + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + lodash: 4.17.21 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/colors@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-color': 3.1.3 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + '@types/prop-types': 15.7.12 + d3-color: 3.1.0 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/core@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-shape': 3.1.6 + d3-color: 3.1.0 + d3-format: 1.4.5 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-shape: 3.2.0 + d3-time-format: 3.0.0 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/legends@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-scale': 4.0.8 + d3-scale: 4.0.2 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/line@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/annotations': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/axes': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/legends': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/scales': 0.87.0 + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/voronoi': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + d3-shape: 3.2.0 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/scales@0.87.0': + dependencies: + '@types/d3-scale': 4.0.8 + '@types/d3-time': 1.1.4 + '@types/d3-time-format': 3.0.4 + d3-scale: 4.0.2 + d3-time: 1.1.0 + d3-time-format: 3.0.0 + lodash: 4.17.21 + + '@nivo/tooltip@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/voronoi@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-delaunay': 6.0.4 + '@types/d3-scale': 4.0.8 + d3-delaunay: 6.0.4 + d3-scale: 4.0.2 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + '@noble/hashes@1.5.0': {} '@nodelib/fs.scandir@2.1.5': @@ -9572,6 +9894,41 @@ snapshots: '@popperjs/core@2.11.8': {} + '@prisma/client@5.16.1': + optional: true + + '@react-spring/animated@9.7.4(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.7.4(react@18.3.1) + '@react-spring/types': 9.7.4 + react: 18.3.1 + + '@react-spring/core@9.7.4(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.4(react@18.3.1) + '@react-spring/shared': 9.7.4(react@18.3.1) + '@react-spring/types': 9.7.4 + react: 18.3.1 + + '@react-spring/rafz@9.7.4': {} + + '@react-spring/shared@9.7.4(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.7.4 + '@react-spring/types': 9.7.4 + react: 18.3.1 + + '@react-spring/types@9.7.4': {} + + '@react-spring/web@9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.4(react@18.3.1) + '@react-spring/core': 9.7.4(react@18.3.1) + '@react-spring/shared': 9.7.4(react@18.3.1) + '@react-spring/types': 9.7.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@remirror/core-constants@3.0.0': {} '@rollup/pluginutils@5.1.0(rollup@4.21.3)': @@ -10387,6 +10744,32 @@ snapshots: '@types/css-modules@1.0.5': {} + '@types/d3-color@3.1.3': {} + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-format@1.4.5': {} + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale-chromatic@3.0.3': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time-format@2.3.4': {} + + '@types/d3-time-format@3.0.4': {} + + '@types/d3-time@1.1.4': {} + + '@types/d3-time@3.0.3': {} + '@types/docker-modem@3.0.6': dependencies: '@types/node': 22.9.3 @@ -11499,6 +11882,59 @@ snapshots: csstype@3.1.3: {} + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-format@1.4.5: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 1.4.5 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 3.0.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@3.0.0: + dependencies: + d3-time: 2.1.1 + + d3-time@1.1.0: {} + + d3-time@2.1.1: + dependencies: + d3-array: 2.12.1 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + damerau-levenshtein@1.0.8: {} data-uri-to-buffer@6.0.2: {} @@ -11585,6 +12021,10 @@ snapshots: rimraf: 3.0.2 slash: 3.0.0 + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -11697,18 +12137,19 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.36.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1): + drizzle-orm@0.36.4(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1): optionalDependencies: '@libsql/client-wasm': 0.14.0 + '@prisma/client': 5.16.1 '@types/better-sqlite3': 7.6.12 '@types/react': 18.3.12 better-sqlite3: 11.5.0 mysql2: 3.11.4 react: 18.3.1 - drizzle-zod@0.5.1(drizzle-orm@0.36.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1))(zod@3.23.8): + drizzle-zod@0.5.1(drizzle-orm@0.36.4(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1))(zod@3.23.8): dependencies: - drizzle-orm: 0.36.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1) + drizzle-orm: 0.36.4(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.12)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.4)(react@18.3.1) zod: 3.23.8 eastasianwidth@0.2.0: {} @@ -12681,6 +13122,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + internmap@1.0.1: {} + + internmap@2.0.3: {} + intl-messageformat@10.7.1: dependencies: '@formatjs/ecma402-abstract': 2.2.0 @@ -14250,6 +14695,8 @@ snapshots: dependencies: glob: 7.2.3 + robust-predicates@3.0.2: {} + rollup@4.21.3: dependencies: '@types/estree': 1.0.5