diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index 208d90a4f..03bc8f6a9 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { useRouter } from "next/navigation"; import type { MantineColor } from "@mantine/core"; import { ActionIcon, Avatar, Badge, Box, Button, Group, Menu, Text, Tooltip } from "@mantine/core"; @@ -153,17 +153,13 @@ const createColumns = (t: TranslationFunction): MRT_ColumnDef[] export function DockerTable({ containers, timestamp }: RouterOutputs["docker"]["getContainers"]) { const t = useI18n(); const tDocker = useScopedI18n("docker"); - const { data } = clientApi.docker.getContainers.useQuery(undefined, { + const { data, isFetching, refetch } = clientApi.docker.getContainers.useQuery(undefined, { initialData: { containers, timestamp }, refetchOnMount: false, }); const relativeTime = useTimeAgo(data.timestamp); - const utils = clientApi.useUtils(); - const [isPending, setIsPending] = useState(false); const mutate = useCallback(() => { - setIsPending(true); - void utils.docker.getContainers - .invalidate() + void refetch() .then(() => { showSuccessNotification({ title: tDocker("action.refresh.notification.success.title"), @@ -175,9 +171,8 @@ export function DockerTable({ containers, timestamp }: RouterOutputs["docker"][" title: tDocker("action.refresh.notification.error.title"), message: tDocker("action.refresh.notification.error.message"), }); - }) - .finally(() => setIsPending(false)); - }, [utils, tDocker]); + }); + }, [refetch, tDocker]); const table = useTranslatedMantineReactTable({ data: data.containers, @@ -203,7 +198,7 @@ export function DockerTable({ containers, timestamp }: RouterOutputs["docker"][" mantineTableHeadCellProps: { style: { padding: "4px 8px" } }, renderRowActions: ({ row }: { row: MRT_Row }) => , renderTopToolbarCustomActions: () => ( - ), diff --git a/packages/api/src/router/widgets/beszel.ts b/packages/api/src/router/widgets/beszel.ts index 54781518d..797de8609 100644 --- a/packages/api/src/router/widgets/beszel.ts +++ b/packages/api/src/router/widgets/beszel.ts @@ -95,13 +95,17 @@ export const beszelRouter = createTRPCRouter({ maxHistoryItems: input.maxHistoryItems, }); const systemsHandler = beszelSystemsRequestHandler.handler(integration, {}); - const [alertsResult, systemsResult] = await Promise.all([ + const [alertsSettled, systemsSettled] = await Promise.allSettled([ alertsHandler.getDataAsync(), systemsHandler.getDataAsync(), ]); + if (alertsSettled.status === "rejected") throw alertsSettled.reason; + const alertsResult = alertsSettled.value; const systemNameMap: Record = {}; - for (const system of systemsResult.data) { - systemNameMap[system.id] = system.name; + if (systemsSettled.status === "fulfilled") { + for (const system of systemsSettled.value.data) { + systemNameMap[system.id] = system.name; + } } return { integrationId: integration.id, diff --git a/packages/api/src/router/widgets/coolify.ts b/packages/api/src/router/widgets/coolify.ts index a876a9ea6..9476039d9 100644 --- a/packages/api/src/router/widgets/coolify.ts +++ b/packages/api/src/router/widgets/coolify.ts @@ -1,27 +1,24 @@ import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const coolifyRouter = createTRPCRouter({ getInstancesInfo: publicProcedure .concat(createManyIntegrationMiddleware("query", "coolify")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = coolifyRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = coolifyRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - instanceInfo: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + instanceInfo: data, + updatedAt: timestamp, + }; + }); }), }); diff --git a/packages/api/src/router/widgets/dns-hole.ts b/packages/api/src/router/widgets/dns-hole.ts index 0d4d673cf..32f15da98 100644 --- a/packages/api/src/router/widgets/dns-hole.ts +++ b/packages/api/src/router/widgets/dns-hole.ts @@ -5,6 +5,7 @@ import { createIntegrationAsync } from "@homarr/integrations"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; export const dnsHoleRouter = createTRPCRouter({ @@ -18,23 +19,13 @@ export const dnsHoleRouter = createTRPCRouter({ }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await dnsHoleRequestHandler.handler(integration, {}).getDataAsync(); + return { + integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, + summary: data, + }; + }); }), enable: protectedProcedure diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index 3503ff591..50aee350e 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -6,6 +6,7 @@ import { downloadClientRequestHandler } from "@homarr/request-handler/downloads" import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) => @@ -23,23 +24,14 @@ export const downloadsRouter = createTRPCRouter({ .concat(createDownloadClientIntegrationMiddleware("query")) .input(z.object({ limitPerIntegration: z.number().default(50) })) .query(async ({ ctx, input }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); - - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - data, - }; - }), - ); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); + const { data, timestamp } = await innerHandler.getDataAsync(); + return { + integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, + data, + }; + }); }), pause: protectedProcedure .meta({ diff --git a/packages/api/src/router/widgets/firewall.ts b/packages/api/src/router/widgets/firewall.ts index 459222f56..6929a68b6 100644 --- a/packages/api/src/router/widgets/firewall.ts +++ b/packages/api/src/router/widgets/firewall.ts @@ -7,94 +7,26 @@ import { } from "@homarr/request-handler/firewall"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; +const firewallMiddleware = createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")); + +// ponytail: can't extract a generic factory here because each handler returns a different data type +const queryFirewall = { getDataAsync: () => Promise<{ data: any; timestamp: Date }> } }>(handler: THandler) => + publicProcedure.concat(firewallMiddleware).query(async ({ ctx }) => + settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await handler.handler(integration, {}).getDataAsync(); + return { + integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, + summary: data as Awaited["getDataAsync"]>>["data"], + }; + }), + ); + export const firewallRouter = createTRPCRouter({ - getFirewallCpuStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallCpuRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - - getFirewallInterfacesStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallInterfacesRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - - getFirewallVersionStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallVersionRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), - - getFirewallMemoryStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = firewallMemoryRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - summary: data, - }; - }), - ); - return results; - }), + getFirewallCpuStatus: queryFirewall(firewallCpuRequestHandler), + getFirewallInterfacesStatus: queryFirewall(firewallInterfacesRequestHandler), + getFirewallVersionStatus: queryFirewall(firewallVersionRequestHandler), + getFirewallMemoryStatus: queryFirewall(firewallMemoryRequestHandler), }); diff --git a/packages/api/src/router/widgets/health-monitoring.ts b/packages/api/src/router/widgets/health-monitoring.ts index 7c45e6c64..990faa238 100644 --- a/packages/api/src/router/widgets/health-monitoring.ts +++ b/packages/api/src/router/widgets/health-monitoring.ts @@ -1,6 +1,7 @@ import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const healthMonitoringRouter = createTRPCRouter({ @@ -16,19 +17,10 @@ export const healthMonitoringRouter = createTRPCRouter({ createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "glances", "mock"), ) .query(async ({ ctx }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = systemInfoRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); - - return { - integrationId: integration.id, - integrationName: integration.name, - healthInfo: data, - updatedAt: timestamp, - }; - }), - ); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data, timestamp } = await systemInfoRequestHandler.handler(integration, {}).getDataAsync(); + return { integrationId: integration.id, integrationName: integration.name, healthInfo: data, updatedAt: timestamp }; + }); }), getClusterHealthStatus: publicProcedure .meta({ diff --git a/packages/api/src/router/widgets/indexer-manager.ts b/packages/api/src/router/widgets/indexer-manager.ts index 54ac3b9a7..54a949dab 100644 --- a/packages/api/src/router/widgets/indexer-manager.ts +++ b/packages/api/src/router/widgets/indexer-manager.ts @@ -6,6 +6,7 @@ import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-ma import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) => @@ -15,18 +16,15 @@ export const indexerManagerRouter = createTRPCRouter({ getIndexersStatus: publicProcedure .concat(createIndexerManagerIntegrationMiddleware("query")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = indexerManagerRequestHandler.handler(integration, {}); - const { data: indexers } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = indexerManagerRequestHandler.handler(integration, {}); + const { data: indexers } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - indexers, - }; - }), - ); - return results; + return { + integrationId: integration.id, + indexers, + }; + }); }), testAllIndexers: protectedProcedure .concat(createIndexerManagerIntegrationMiddleware("interact")) diff --git a/packages/api/src/router/widgets/media-release.ts b/packages/api/src/router/widgets/media-release.ts index 4da950844..f05a46142 100644 --- a/packages/api/src/router/widgets/media-release.ts +++ b/packages/api/src/router/widgets/media-release.ts @@ -2,28 +2,27 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { mediaReleaseRequestHandler } from "@homarr/request-handler/media-release"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const mediaReleaseRouter = createTRPCRouter({ getMediaReleases: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaReleaseRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + const results = await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = mediaReleaseRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - releases: data, - }; - }), - ); + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + releases: data, + }; + }); return results.flatMap((result) => result.releases.map((release) => ({ ...release, diff --git a/packages/api/src/router/widgets/media-requests.ts b/packages/api/src/router/widgets/media-requests.ts index a5d51ac9f..99c4906e3 100644 --- a/packages/api/src/router/widgets/media-requests.ts +++ b/packages/api/src/router/widgets/media-requests.ts @@ -7,6 +7,7 @@ import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-re import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc"; export const mediaRequestsRouter = createTRPCRouter({ @@ -20,20 +21,10 @@ export const mediaRequestsRouter = createTRPCRouter({ }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); - const { data } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - }, - data, - }; - }), - ); + const results = await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data } = await mediaRequestListRequestHandler.handler(integration, {}).getDataAsync(); + return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data }; + }); return results .flatMap(({ data, integration }) => data.map((request) => ({ @@ -62,20 +53,10 @@ export const mediaRequestsRouter = createTRPCRouter({ }) .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaRequestStatsRequestHandler.handler(integration, {}); - const { data } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - }, - data, - }; - }), - ); + const results = await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data } = await mediaRequestStatsRequestHandler.handler(integration, {}).getDataAsync(); + return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data }; + }); return { stats: results.flatMap((result) => result.data.stats), users: results diff --git a/packages/api/src/router/widgets/media-server.ts b/packages/api/src/router/widgets/media-server.ts index a1eab6c82..e673cefac 100644 --- a/packages/api/src/router/widgets/media-server.ts +++ b/packages/api/src/router/widgets/media-server.ts @@ -5,6 +5,7 @@ import { mediaServerRequestHandler } from "@homarr/request-handler/media-server" import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const createMediaServerIntegrationMiddleware = (action: IntegrationAction) => @@ -22,18 +23,9 @@ export const mediaServerRouter = createTRPCRouter({ .concat(createMediaServerIntegrationMiddleware("query")) .input(z.object({ showOnlyPlaying: z.boolean() })) .query(async ({ ctx, input }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = mediaServerRequestHandler.handler(integration, { - showOnlyPlaying: input.showOnlyPlaying, - }); - const { data } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationKind: integration.kind, - sessions: data, - }; - }), - ); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const { data } = await mediaServerRequestHandler.handler(integration, { showOnlyPlaying: input.showOnlyPlaying }).getDataAsync(); + return { integrationId: integration.id, integrationKind: integration.kind, sessions: data }; + }); }), }); diff --git a/packages/api/src/router/widgets/network-controller.ts b/packages/api/src/router/widgets/network-controller.ts index 1b75645af..61257d1ec 100644 --- a/packages/api/src/router/widgets/network-controller.ts +++ b/packages/api/src/router/widgets/network-controller.ts @@ -2,28 +2,26 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const networkControllerRouter = createTRPCRouter({ summary: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = networkControllerRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = networkControllerRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - }, - summary: data, - updatedAt: timestamp, - }; - }), - ); - return results; + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + }, + summary: data, + updatedAt: timestamp, + }; + }); }), }); diff --git a/packages/api/src/router/widgets/notifications.ts b/packages/api/src/router/widgets/notifications.ts index fe587e78a..bae8ba9fd 100644 --- a/packages/api/src/router/widgets/notifications.ts +++ b/packages/api/src/router/widgets/notifications.ts @@ -3,6 +3,7 @@ import { notificationsRequestHandler } from "@homarr/request-handler/notificatio import type { IntegrationAction } from "../../middlewares/integration"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const createNotificationsIntegrationMiddleware = (action: IntegrationAction) => @@ -12,21 +13,19 @@ export const notificationsRouter = createTRPCRouter({ getNotifications: publicProcedure .unstable_concat(createNotificationsIntegrationMiddleware("query")) .query(async ({ ctx }) => { - return await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = notificationsRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = notificationsRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integration: { - id: integration.id, - name: integration.name, - kind: integration.kind, - updatedAt: timestamp, - }, - data, - }; - }), - ); + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + data, + }; + }); }), }); diff --git a/packages/api/src/router/widgets/speedtest-tracker.ts b/packages/api/src/router/widgets/speedtest-tracker.ts index a75dcfd93..01e7fb58b 100644 --- a/packages/api/src/router/widgets/speedtest-tracker.ts +++ b/packages/api/src/router/widgets/speedtest-tracker.ts @@ -1,27 +1,24 @@ import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const speedtestTrackerRouter = createTRPCRouter({ getDashboard: publicProcedure .concat(createManyIntegrationMiddleware("query", "speedtestTracker")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, + }; + }); }), }); diff --git a/packages/api/src/router/widgets/tracearr.ts b/packages/api/src/router/widgets/tracearr.ts index 94b612a4a..5075c990b 100644 --- a/packages/api/src/router/widgets/tracearr.ts +++ b/packages/api/src/router/widgets/tracearr.ts @@ -1,25 +1,22 @@ import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const tracearrRouter = createTRPCRouter({ getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = tracearrRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = tracearrRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, + }; + }); }), }); diff --git a/packages/api/src/router/widgets/umami.ts b/packages/api/src/router/widgets/umami.ts index ecfe42436..3994f398e 100644 --- a/packages/api/src/router/widgets/umami.ts +++ b/packages/api/src/router/widgets/umami.ts @@ -13,6 +13,7 @@ import { } from "@homarr/request-handler/umami"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; const logger = createLogger({ module: "umami-router" }); @@ -38,26 +39,22 @@ export const umamiRouter = createTRPCRouter({ ) .concat(createManyIntegrationMiddleware("query", "umami")) .query(async ({ ctx, input }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = umamiRequestHandler.handler(integration, { - websiteId: input.websiteId, - timeFrame: input.timeFrame, - eventName: input.eventName, - }); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = umamiRequestHandler.handler(integration, { + websiteId: input.websiteId, + timeFrame: input.timeFrame, + eventName: input.eventName, + }); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - visitorStats: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + visitorStats: data, + updatedAt: timestamp, + }; + }); }), getEventNames: publicProcedure diff --git a/packages/api/src/router/widgets/ups.ts b/packages/api/src/router/widgets/ups.ts index 02878665d..16c6312dd 100644 --- a/packages/api/src/router/widgets/ups.ts +++ b/packages/api/src/router/widgets/ups.ts @@ -2,27 +2,24 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { upsSummariesRequestHandler } from "@homarr/request-handler/ups"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const upsRouter = createTRPCRouter({ getSummaries: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups"))) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = upsSummariesRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = upsSummariesRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - summaries: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + summaries: data, + updatedAt: timestamp, + }; + }); }), }); diff --git a/packages/api/src/router/widgets/uptime-kuma.ts b/packages/api/src/router/widgets/uptime-kuma.ts index ca3a6b01e..6df14090c 100644 --- a/packages/api/src/router/widgets/uptime-kuma.ts +++ b/packages/api/src/router/widgets/uptime-kuma.ts @@ -1,27 +1,24 @@ import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { settleIntegrationQueries } from "../../settle-integrations"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const uptimeKumaRouter = createTRPCRouter({ getDashboard: publicProcedure .concat(createManyIntegrationMiddleware("query", "uptimeKuma")) .query(async ({ ctx }) => { - const results = await Promise.all( - ctx.integrations.map(async (integration) => { - const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getDataAsync(); + return await settleIntegrationQueries(ctx.integrations, async (integration) => { + const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getDataAsync(); - return { - integrationId: integration.id, - integrationName: integration.name, - integrationUrl: integration.url, - dashboard: data, - updatedAt: timestamp, - }; - }), - ); - - return results; + return { + integrationId: integration.id, + integrationName: integration.name, + integrationUrl: integration.url, + dashboard: data, + updatedAt: timestamp, + }; + }); }), }); diff --git a/packages/api/src/settle-integrations.ts b/packages/api/src/settle-integrations.ts new file mode 100644 index 000000000..a359f7544 --- /dev/null +++ b/packages/api/src/settle-integrations.ts @@ -0,0 +1,29 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; + +const logger = createLogger({ module: "settleIntegrations" }); + +interface IntegrationLike { + id: string; + name: string; + kind: string; +} + +export async function settleIntegrationQueries( + integrations: TIntegration[], + fn: (integration: TIntegration) => Promise, +): Promise { + const settled = await Promise.allSettled(integrations.map(fn)); + return settled.flatMap((result, index) => { + if (result.status === "fulfilled") return [result.value]; + const integration = integrations[index]; + logger.warn( + new ErrorWithMetadata( + "Integration query failed", + { integrationId: integration?.id, integrationKind: integration?.kind }, + { cause: result.reason }, + ), + ); + return []; + }); +} diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 506d88b85..c21b85245 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -156,34 +156,8 @@ export const removeQueryCacheAsync = async (userId: string, boardId: string) => await getSetClient.del(queryCacheKey(userId, boardId)); }; -/** - * Invalidates all cached data for a given integration by deleting every Redis key - * that starts with `integration:${integrationId}:` as well as the integration's - * session store entry. This ensures that stale data from a misconfigured or - * updated integration is never served from cache. - */ export const invalidateIntegrationCacheAsync = async (integrationId: string): Promise => { - const client = getSetClient as typeof getSetClient | null; - if (!client) { - return; - } - - const patterns = [`integration:${integrationId}:*`, `session-store:${integrationId}`]; - let deletedCount = 0; - for (const pattern of patterns) { - let cursor = "0"; - do { - const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", 200); - cursor = nextCursor; - if (keys.length > 0) { - await client.del(...keys); - deletedCount += keys.length; - } - } while (cursor !== "0"); - } - if (deletedCount > 0) { - logger.info("Invalidated integration cache", { integrationId, deletedKeys: deletedCount }); - } + await getSetClient.del(`session-store:${integrationId}`); }; /** diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 2dc01040d..f1ab6e74c 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -111,10 +111,11 @@ export default function DownloadClientsWidget({ return statuses; }, [currentItems]); - //Get API mutation functions - const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(); - const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(); - const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(); + const utils = clientApi.useUtils(); + const invalidateDownloads = { onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate() }; + const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation(invalidateDownloads); + const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(invalidateDownloads); + const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(invalidateDownloads); //Flatten Data array for which each element has it's integration, data (base + calculated) and actions. Memoized on data subscription const data = useMemo( diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 4b5211170..90011d768 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -29,10 +29,10 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget } }, []); - const firewallsCpuData = useUpdatingCpuStatus(integrationIds); - const firewallsMemoryData = useUpdatingMemoryStatus(integrationIds); - const firewallsVersionData = useUpdatingVersionStatus(integrationIds); - const firewallsInterfacesData = useUpdatingInterfacesStatus(integrationIds); + const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ integrationIds }); + const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ integrationIds }); + const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ integrationIds }); + const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ integrationIds }); const initialSelectedFirewall = firewallsVersionData[0] ? firewallsVersionData[0].integration.id : "undefined"; const isTiny = width < 256; @@ -182,37 +182,6 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget ); } -export const useUpdatingCpuStatus = (integrationIds: string[]) => { - const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ - integrationIds, - }); - - return firewallsCpuData; -}; - -export const useUpdatingMemoryStatus = (integrationIds: string[]) => { - const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ - integrationIds, - }); - - return firewallsMemoryData; -}; - -export const useUpdatingVersionStatus = (integrationIds: string[]) => { - const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ - integrationIds, - }); - return firewallsVersionData; -}; - -export const useUpdatingInterfacesStatus = (integrationIds: string[]) => { - const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ - integrationIds, - }); - - return firewallsInterfacesData; -}; - export function formatBitsPerSec(bytes: number, decimals: number): string { if (bytes === 0) return "0 b/s"; diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index efa32f911..9b20ce671 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -198,7 +198,10 @@ interface DecisionButtonsProps { } const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) => { - const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation(); + const utils = clientApi.useUtils(); + const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation({ + onSettled: () => void utils.widget.mediaRequests.getLatestRequests.invalidate(), + }); const t = useScopedI18n("widget.mediaRequests-requestList"); const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => { mutateRequestAnswer({ diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index 352db4cd3..4cacac9ab 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Box, Group, Stack } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; @@ -43,7 +43,10 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo { cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[] >(() => (data[0] ? [toChartItem(data[0].healthInfo)] : [])); + const lastUpdatedAt = useRef(dataUpdatedAt); useEffect(() => { + if (dataUpdatedAt === lastUpdatedAt.current) return; + lastUpdatedAt.current = dataUpdatedAt; const firstItem = data[0]; if (!firstItem) return; setItems((prev) => [...prev, toChartItem(firstItem.healthInfo)].slice(-MAX_QUEUE_SIZE));