From 0f8d9edb3e450f0b2d656bf926c2327fe7c824bf Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Fri, 11 Oct 2024 23:47:07 +0200 Subject: [PATCH] refactor: replace serverdata with suspense query (#1265) * refactor: replace serverdata with suspense query * fix: deepsource issues --- .../[locale]/boards/(content)/_creator.tsx | 1 - .../src/app/[locale]/boards/[name]/layout.tsx | 1 - .../app/[locale]/boards/_layout-creator.tsx | 31 ++--- .../components/board/items/item-content.tsx | 7 +- packages/api/src/router/widgets/calendar.ts | 3 +- packages/widgets/src/app/index.ts | 2 +- packages/widgets/src/calendar/component.tsx | 17 ++- packages/widgets/src/calendar/index.ts | 6 +- packages/widgets/src/calendar/serverData.ts | 35 ------ packages/widgets/src/definition.ts | 49 +------- .../src/dns-hole/controls/component.tsx | 115 +++++++++++++----- .../widgets/src/dns-hole/controls/index.ts | 6 +- .../src/dns-hole/controls/serverData.ts | 29 ----- .../src/dns-hole/summary/component.tsx | 54 +++++--- .../widgets/src/dns-hole/summary/index.ts | 6 +- .../src/dns-hole/summary/serverData.ts | 29 ----- packages/widgets/src/downloads/component.tsx | 66 +++++----- packages/widgets/src/downloads/index.ts | 6 +- packages/widgets/src/downloads/serverData.ts | 21 ---- .../src/health-monitoring/component.tsx | 22 ++-- .../widgets/src/health-monitoring/index.ts | 6 +- .../src/health-monitoring/serverData.ts | 27 ---- packages/widgets/src/index.tsx | 2 - .../widgets/src/indexer-manager/component.tsx | 27 ++-- packages/widgets/src/indexer-manager/index.ts | 6 +- .../widgets/src/indexer-manager/serverData.ts | 27 ---- .../src/media-requests/list/component.tsx | 10 +- .../widgets/src/media-requests/list/index.ts | 6 +- .../src/media-requests/list/serverData.ts | 22 ---- .../src/media-requests/stats/component.tsx | 8 +- .../widgets/src/media-requests/stats/index.ts | 6 +- .../src/media-requests/stats/serverData.ts | 25 ---- .../widgets/src/media-server/component.tsx | 40 +++--- packages/widgets/src/media-server/index.ts | 6 +- .../widgets/src/media-server/serverData.ts | 21 ---- packages/widgets/src/rssFeed/component.tsx | 24 +++- packages/widgets/src/rssFeed/index.ts | 6 +- packages/widgets/src/rssFeed/serverData.ts | 21 ---- packages/widgets/src/server/client.tsx | 13 -- packages/widgets/src/server/provider.tsx | 74 ----------- packages/widgets/src/server/runner.tsx | 51 -------- 41 files changed, 288 insertions(+), 646 deletions(-) delete mode 100644 packages/widgets/src/calendar/serverData.ts delete mode 100644 packages/widgets/src/dns-hole/controls/serverData.ts delete mode 100644 packages/widgets/src/dns-hole/summary/serverData.ts delete mode 100644 packages/widgets/src/downloads/serverData.ts delete mode 100644 packages/widgets/src/health-monitoring/serverData.ts delete mode 100644 packages/widgets/src/indexer-manager/serverData.ts delete mode 100644 packages/widgets/src/media-requests/list/serverData.ts delete mode 100644 packages/widgets/src/media-requests/stats/serverData.ts delete mode 100644 packages/widgets/src/media-server/serverData.ts delete mode 100644 packages/widgets/src/rssFeed/serverData.ts delete mode 100644 packages/widgets/src/server/client.tsx delete mode 100644 packages/widgets/src/server/provider.tsx delete mode 100644 packages/widgets/src/server/runner.tsx diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index 6a6be9b42..15f386719 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -28,7 +28,6 @@ export const createBoardContentPage = >( layout: createBoardLayout({ headerActions: , getInitialBoardAsync: getInitialBoard, - isBoardContentPage: true, }), // eslint-disable-next-line no-restricted-syntax page: async () => { diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/layout.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/layout.tsx index 42107bf2d..1f129e4c1 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/layout.tsx @@ -8,5 +8,4 @@ export default createBoardLayout<{ locale: string; name: string }>({ async getInitialBoardAsync({ name }) { return await api.board.getBoardByName({ name }); }, - isBoardContentPage: false, }); diff --git a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx index 56dabdd88..4e8edd149 100644 --- a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx @@ -4,7 +4,6 @@ import { AppShellMain } from "@mantine/core"; import { TRPCError } from "@trpc/server"; import { logger } from "@homarr/log"; -import { GlobalItemServerDataRunner } from "@homarr/widgets"; import { MainHeader } from "~/components/layout/header"; import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo"; @@ -18,13 +17,11 @@ import { BoardMantineProvider } from "./(content)/_theme"; interface CreateBoardLayoutProps { headerActions: JSX.Element; getInitialBoardAsync: (params: TParams) => Promise; - isBoardContentPage: boolean; } export const createBoardLayout = ({ headerActions, getInitialBoardAsync: getInitialBoard, - isBoardContentPage, }: CreateBoardLayoutProps) => { const Layout = async ({ params, @@ -42,21 +39,19 @@ export const createBoardLayout = ({ }); return ( - - - - - - } - actions={headerActions} - hasNavigation={false} - /> - {children} - - - - + + + + + } + actions={headerActions} + hasNavigation={false} + /> + {children} + + + ); }; diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 2af6b3154..930cde473 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -4,7 +4,7 @@ import { QueryErrorResetBoundary } from "@tanstack/react-query"; import combineClasses from "clsx"; import { ErrorBoundary } from "react-error-boundary"; -import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, useServerDataFor } from "@homarr/widgets"; +import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets"; import { WidgetError } from "@homarr/widgets/errors"; import type { Item } from "~/app/[locale]/boards/_types"; @@ -53,7 +53,6 @@ interface InnerContentProps { const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const board = useRequiredBoard(); const [isEditMode] = useEditMode(); - const serverData = useServerDataFor(item.id); const Comp = loadWidgetDynamic(item.kind); const options = reduceWidgetOptionsWithDefaultValues(item.kind, item.options); const newItem = { ...item, options }; @@ -61,8 +60,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const updateOptions = ({ newOptions }: { newOptions: Record }) => updateItemOptions({ itemId: item.id, newOptions }); - if (!serverData?.isReady) return null; - return ( {({ reset }) => ( @@ -79,8 +76,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { { - return await Promise.all( + const result = await Promise.all( ctx.integrations.flatMap(async (integration) => { const cache = createItemAndIntegrationChannel("calendar", integration.id); return await cache.getAsync(); }), ); + return result.filter((item) => item !== null).flatMap((item) => item.data); }), }); diff --git a/packages/widgets/src/app/index.ts b/packages/widgets/src/app/index.ts index 506f654e5..66d7a0ecb 100644 --- a/packages/widgets/src/app/index.ts +++ b/packages/widgets/src/app/index.ts @@ -3,7 +3,7 @@ import { IconApps, IconDeviceDesktopX } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("app", { +export const { definition, componentLoader } = createWidgetDefinition("app", { icon: IconApps, options: optionsBuilder.from((factory) => ({ appId: factory.app(), diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index 1e21122e9..d773c7fe5 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -11,7 +11,20 @@ import type { WidgetComponentProps } from "../definition"; import { CalendarDay } from "./calender-day"; import classes from "./component.module.css"; -export default function CalendarWidget({ isEditMode, serverData }: WidgetComponentProps<"calendar">) { +export default function CalendarWidget({ isEditMode, integrationIds, itemId }: WidgetComponentProps<"calendar">) { + const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery( + { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + itemId: itemId!, + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); const [month, setMonth] = useState(new Date()); const params = useParams(); const locale = params.locale as string; @@ -68,7 +81,7 @@ export default function CalendarWidget({ isEditMode, serverData }: WidgetCompone }, }} renderDay={(date) => { - const eventsForDate = (serverData?.initialData ?? []).filter((event) => dayjs(event.date).isSame(date, "day")); + const eventsForDate = events.filter((event) => dayjs(event.date).isSame(date, "day")); return ; }} /> diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts index ff7d8d8a5..e3243038c 100644 --- a/packages/widgets/src/calendar/index.ts +++ b/packages/widgets/src/calendar/index.ts @@ -6,7 +6,7 @@ import { z } from "@homarr/validation"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("calendar", { +export const { definition, componentLoader } = createWidgetDefinition("calendar", { icon: IconCalendar, options: optionsBuilder.from((factory) => ({ filterPastMonths: factory.number({ @@ -19,6 +19,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef }), })), supportedIntegrations: getIntegrationKindsByCategory("calendar"), -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/calendar/serverData.ts b/packages/widgets/src/calendar/serverData.ts deleted file mode 100644 index 907efba2e..000000000 --- a/packages/widgets/src/calendar/serverData.ts +++ /dev/null @@ -1,35 +0,0 @@ -"use server"; - -import type { RouterOutputs } from "@homarr/api"; -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../definition"; - -export default async function getServerDataAsync({ integrationIds, itemId }: WidgetProps<"calendar">) { - if (!itemId) { - return { - initialData: [], - }; - } - try { - const data = await api.widget.calendar.findAllEvents({ - integrationIds, - itemId, - }); - - return { - initialData: data - .filter( - ( - item, - ): item is Exclude, undefined> => - item !== null, - ) - .flatMap((item) => item.data), - }; - } catch { - return { - initialData: [], - }; - } -} diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts index 46346bddb..77238b469 100644 --- a/packages/widgets/src/definition.ts +++ b/packages/widgets/src/definition.ts @@ -8,57 +8,22 @@ import type { TablerIcon } from "@homarr/ui"; import type { WidgetImports } from "."; import type { inferOptionsFromDefinition, WidgetOptionsRecord } from "./options"; -type ServerDataLoader = () => Promise<{ - default: (props: WidgetProps) => Promise>; -}>; - const createWithDynamicImport = - < - TKind extends WidgetKind, - TDefinition extends WidgetDefinition, - TServerDataLoader extends ServerDataLoader | undefined, - >( - kind: TKind, - definition: TDefinition, - serverDataLoader: TServerDataLoader, - ) => - ( - componentLoader: () => LoaderComponent< - WidgetComponentProps & - (TServerDataLoader extends ServerDataLoader - ? { - serverData: Awaited>["default"]>>; - } - : never) - >, - ) => ({ - definition: { - ...definition, - kind, - }, - kind, - serverDataLoader, - componentLoader, - }); - -const createWithServerData = (kind: TKind, definition: TDefinition) => - >(serverDataLoader: TServerDataLoader) => ({ + (componentLoader: () => LoaderComponent>) => ({ definition: { ...definition, kind, }, kind, - serverDataLoader, - withDynamicImport: createWithDynamicImport(kind, definition, serverDataLoader), + componentLoader, }); export const createWidgetDefinition = ( kind: TKind, definition: TDefinition, ) => ({ - withServerData: createWithServerData(kind, definition), - withDynamicImport: createWithDynamicImport(kind, definition, undefined), + withDynamicImport: createWithDynamicImport(kind, definition), }); export interface WidgetDefinition { @@ -83,15 +48,7 @@ export interface WidgetProps { itemId: string | undefined; // undefined when in preview mode } -type inferServerDataForKind = WidgetImports[TKind] extends { - serverDataLoader: ServerDataLoader; -} - ? Awaited>["default"]>> - : undefined; - export type WidgetComponentProps = WidgetProps & { - serverData?: inferServerDataForKind; -} & { boardId: string | undefined; // undefined when in preview mode isEditMode: boolean; setOptions: ({ diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx index b5f71b46e..06e41ea7b 100644 --- a/packages/widgets/src/dns-hole/controls/component.tsx +++ b/packages/widgets/src/dns-hole/controls/component.tsx @@ -39,16 +39,25 @@ export default function DnsHoleControlsWidget({ options, integrationIds, isEditMode, - serverData, }: WidgetComponentProps) { // DnsHole integrations with interaction permissions const integrationsWithInteractions = useIntegrationsWithInteractAccess() .map(({ id }) => id) .filter((id) => integrationIds.includes(id)); - // Initial summaries, null summary means disconnected, undefined status means processing - const [summaries, setSummaries] = useState(serverData?.initialData ?? []); - + const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery( + { + widgetKind: "dnsHoleControls", + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); + const utils = clientApi.useUtils(); // Subscribe to summary updates clientApi.widget.dnsHole.subscribeToSummary.useSubscription( { @@ -57,8 +66,20 @@ export default function DnsHoleControlsWidget({ }, { onData: (data) => { - setSummaries((prevSummaries) => - prevSummaries.map((summary) => (summary.integration.id === data.integration.id ? data : summary)), + utils.widget.dnsHole.summary.setData( + { + widgetKind: "dnsHoleControls", + integrationIds, + }, + (prevData) => { + if (!prevData) return undefined; + + const newData = prevData.map((summary) => + summary.integration.id === data.integration.id ? { ...summary, summary: data.summary } : summary, + ); + + return newData; + }, ); }, }, @@ -67,39 +88,77 @@ export default function DnsHoleControlsWidget({ // Mutations for dnsHole state, set to undefined on click, and change again on settle const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({ onSettled: (_, error, { integrationId }) => { - setSummaries((prevSummaries) => - prevSummaries.map((data) => ({ - ...data, - summary: - data.integration.id === integrationId && data.summary - ? { ...data.summary, status: error ? "disabled" : "enabled" } - : data.summary, - })), + utils.widget.dnsHole.summary.setData( + { + widgetKind: "dnsHoleControls", + integrationIds, + }, + (prevData) => { + if (!prevData) return []; + + return prevData.map((item) => + item.integration.id === integrationId && item.summary + ? { + ...item, + summary: { + ...item.summary, + status: error ? "disabled" : "enabled", + }, + } + : item, + ); + }, ); }, }); const { mutate: disableDns } = clientApi.widget.dnsHole.disable.useMutation({ onSettled: (_, error, { integrationId }) => { - setSummaries((prevSummaries) => - prevSummaries.map((data) => ({ - ...data, - summary: - data.integration.id === integrationId && data.summary - ? { ...data.summary, status: error ? "enabled" : "disabled" } - : data.summary, - })), + utils.widget.dnsHole.summary.setData( + { + widgetKind: "dnsHoleControls", + integrationIds, + }, + (prevData) => { + if (!prevData) return []; + + return prevData.map((item) => + item.integration.id === integrationId && item.summary + ? { + ...item, + summary: { + ...item.summary, + status: error ? "enabled" : "disabled", + }, + } + : item, + ); + }, ); }, }); const toggleDns = (integrationId: string) => { const integrationStatus = summaries.find(({ integration }) => integration.id === integrationId); if (!integrationStatus?.summary?.status) return; - setSummaries((prevSummaries) => - prevSummaries.map((data) => ({ - ...data, - summary: - data.integration.id === integrationId && data.summary ? { ...data.summary, status: undefined } : data.summary, - })), + utils.widget.dnsHole.summary.setData( + { + widgetKind: "dnsHoleControls", + integrationIds, + }, + (prevData) => { + if (!prevData) return []; + + return prevData.map((item) => + item.integration.id === integrationId && item.summary + ? { + ...item, + summary: { + ...item.summary, + status: undefined, + }, + } + : item, + ); + }, ); if (integrationStatus.summary.status === "enabled") { disableDns({ integrationId, duration: 0 }); diff --git a/packages/widgets/src/dns-hole/controls/index.ts b/packages/widgets/src/dns-hole/controls/index.ts index b190b1b24..27f126ee9 100644 --- a/packages/widgets/src/dns-hole/controls/index.ts +++ b/packages/widgets/src/dns-hole/controls/index.ts @@ -7,7 +7,7 @@ import { optionsBuilder } from "../../options"; export const widgetKind = "dnsHoleControls"; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition(widgetKind, { +export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { icon: IconDeviceGamepad, options: optionsBuilder.from((factory) => ({ showToggleAllButtons: factory.switch({ @@ -21,6 +21,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef message: (t) => t("widget.dnsHoleControls.error.internalServerError"), }, }, -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/dns-hole/controls/serverData.ts b/packages/widgets/src/dns-hole/controls/serverData.ts deleted file mode 100644 index 8212b9b9d..000000000 --- a/packages/widgets/src/dns-hole/controls/serverData.ts +++ /dev/null @@ -1,29 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import { widgetKind } from "."; -import type { WidgetProps } from "../../definition"; - -export default async function getServerDataAsync({ integrationIds }: WidgetProps) { - if (integrationIds.length === 0) { - return { - initialData: [], - }; - } - - try { - const currentDns = await api.widget.dnsHole.summary({ - widgetKind, - integrationIds, - }); - - return { - initialData: currentDns, - }; - } catch { - return { - initialData: [], - }; - } -} diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx index 513ecc712..650e144bc 100644 --- a/packages/widgets/src/dns-hole/summary/component.tsx +++ b/packages/widgets/src/dns-hole/summary/component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import type { BoxProps } from "@mantine/core"; import { Avatar, AvatarGroup, Box, Card, Flex, Stack, Text, Tooltip } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; @@ -20,12 +20,20 @@ import { widgetKind } from "."; import type { WidgetComponentProps, WidgetProps } from "../../definition"; import { NoIntegrationSelectedError } from "../../errors"; -export default function DnsHoleSummaryWidget({ - options, - integrationIds, - serverData, -}: WidgetComponentProps) { - const [summaries, setSummaries] = useState(serverData?.initialData ?? []); +export default function DnsHoleSummaryWidget({ options, integrationIds }: WidgetComponentProps) { + const [summaries] = clientApi.widget.dnsHole.summary.useSuspenseQuery( + { + widgetKind, + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, + ); + const utils = clientApi.useUtils(); const t = useI18n(); @@ -36,8 +44,21 @@ export default function DnsHoleSummaryWidget({ }, { onData: (data) => { - setSummaries((prevSummaries) => - prevSummaries.map((summary) => (summary.integration.id === data.integration.id ? data : summary)), + utils.widget.dnsHole.summary.setData( + { + widgetKind, + integrationIds, + }, + (prevData) => { + if (!prevData) { + return undefined; + } + + const newData = prevData.map((item) => + item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, + ); + return newData; + }, ); }, }, @@ -46,17 +67,10 @@ export default function DnsHoleSummaryWidget({ const data = useMemo( () => summaries - .filter( - ( - pair, - ): pair is { - integration: typeof pair.integration; - timestamp: typeof pair.timestamp; - summary: DnsHoleSummary; - } => pair.summary !== null && Math.abs(dayjs(pair.timestamp).diff()) < 30000, - ) - .flatMap(({ summary }) => summary), - [summaries, serverData], + .filter((pair) => Math.abs(dayjs(pair.timestamp).diff()) < 30000) + .flatMap(({ summary }) => summary) + .filter((summary) => summary !== null), + [summaries], ); if (integrationIds.length === 0) { diff --git a/packages/widgets/src/dns-hole/summary/index.ts b/packages/widgets/src/dns-hole/summary/index.ts index d1c3ff18a..25246d23f 100644 --- a/packages/widgets/src/dns-hole/summary/index.ts +++ b/packages/widgets/src/dns-hole/summary/index.ts @@ -7,7 +7,7 @@ import { optionsBuilder } from "../../options"; export const widgetKind = "dnsHoleSummary"; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition(widgetKind, { +export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { icon: IconAd, options: optionsBuilder.from((factory) => ({ usePiHoleColors: factory.switch({ @@ -28,6 +28,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef message: (t) => t("widget.dnsHoleSummary.error.internalServerError"), }, }, -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/dns-hole/summary/serverData.ts b/packages/widgets/src/dns-hole/summary/serverData.ts deleted file mode 100644 index 8212b9b9d..000000000 --- a/packages/widgets/src/dns-hole/summary/serverData.ts +++ /dev/null @@ -1,29 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import { widgetKind } from "."; -import type { WidgetProps } from "../../definition"; - -export default async function getServerDataAsync({ integrationIds }: WidgetProps) { - if (integrationIds.length === 0) { - return { - initialData: [], - }; - } - - try { - const currentDns = await api.widget.dnsHole.summary({ - widgetKind, - integrationIds, - }); - - return { - initialData: currentDns, - }; - } catch { - return { - initialData: [], - }; - } -} diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 7fafc932a..329565e3f 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -21,7 +21,7 @@ import { Title, Tooltip, } from "@mantine/core"; -import { useDisclosure, useListState, useTimeout } from "@mantine/hooks"; +import { useDisclosure, useTimeout } from "@mantine/hooks"; import type { IconProps } from "@tabler/icons-react"; import { IconAlertTriangle, @@ -40,9 +40,6 @@ import { MantineReactTable, useMantineReactTable } from "mantine-react-table"; import { clientApi } from "@homarr/api/client"; import { useIntegrationsWithInteractAccess } from "@homarr/auth/client"; import { humanFileSize } from "@homarr/common"; -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema/sqlite"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus, @@ -91,30 +88,35 @@ export default function DownloadClientsWidget({ isEditMode, integrationIds, options, - serverData, setOptions, }: WidgetComponentProps<"downloads">) { const integrationsWithInteractions = useIntegrationsWithInteractAccess().flatMap(({ id }) => integrationIds.includes(id) ? [id] : [], ); - const [currentItems, currentItemsHandlers] = useListState<{ - integration: Modify }>; - timestamp: Date; - data: DownloadClientJobsAndStatus | null; - }>( - //Automatically invalidate data older than 30 seconds - serverData?.initialData?.map((item) => - dayjs().diff(item.timestamp) < invalidateTime ? item : { ...item, timestamp: new Date(0), data: null }, - ) ?? [], + const [currentItems] = clientApi.widget.downloads.getJobsAndStatuses.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + select(data) { + return data.map((item) => + dayjs().diff(item.timestamp) < invalidateTime ? item : { ...item, timestamp: new Date(0), data: null }, + ); + }, + }, ); + const utils = clientApi.useUtils(); //Invalidate all data after no update for 30 seconds using timer const invalidationTimer = useTimeout( () => { - currentItemsHandlers.applyWhere( - () => true, - (item) => ({ ...item, timestamp: new Date(0), data: null }), + utils.widget.downloads.getJobsAndStatuses.setData({ integrationIds }, (prevData) => + prevData?.map((item) => ({ ...item, timestamp: new Date(0), data: null })), ); }, invalidateTime, @@ -146,20 +148,24 @@ export default function DownloadClientsWidget({ //Don't update already invalid data (new Date (0)) .filter(({ timestamp }) => dayjs().diff(timestamp) > invalidateTime && timestamp > new Date(0)) .map(({ integration }) => integration.id); - currentItemsHandlers.applyWhere( - ({ integration }) => invalidIndexes.includes(integration.id), - //Set date to now so it won't update that integration for at least 30 seconds - (item) => ({ ...item, timestamp: new Date(0), data: null }), + utils.widget.downloads.getJobsAndStatuses.setData({ integrationIds }, (prevData) => + prevData?.map((item) => + invalidIndexes.includes(item.integration.id) ? item : { ...item, timestamp: new Date(0), data: null }, + ), ); - //Find id to update - const updateIndex = currentItems.findIndex((pair) => pair.integration.id === data.integration.id); - if (updateIndex >= 0) { - //Update found index - currentItemsHandlers.setItem(updateIndex, data); - } else if (integrationIds.includes(data.integration.id)) { - //Append index not found (new integration) - currentItemsHandlers.append(data); - } + utils.widget.downloads.getJobsAndStatuses.setData({ integrationIds }, (prevData) => { + const updateIndex = currentItems.findIndex((pair) => pair.integration.id === data.integration.id); + if (updateIndex >= 0) { + //Update found index + return prevData?.map((pair, index) => (index === updateIndex ? data : pair)); + } else if (integrationIds.includes(data.integration.id)) { + //Append index not found (new integration) + return [...(prevData ?? []), data]; + } + + return undefined; + }); + //Reset no update timer invalidationTimer.clear(); invalidationTimer.start(); diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index 1aa15f359..b6ea1ef6a 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -31,7 +31,7 @@ const columnsSort = columnsList.filter((column) => sortingExclusion.some((exclusion) => exclusion !== column), ) as Exclude; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("downloads", { +export const { definition, componentLoader } = createWidgetDefinition("downloads", { icon: IconDownload, options: optionsBuilder.from( (factory) => ({ @@ -105,6 +105,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef }, ), supportedIntegrations: getIntegrationKindsByCategory("downloadClient"), -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/downloads/serverData.ts b/packages/widgets/src/downloads/serverData.ts deleted file mode 100644 index 8259fc5ac..000000000 --- a/packages/widgets/src/downloads/serverData.ts +++ /dev/null @@ -1,21 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../definition"; - -export default async function getServerDataAsync({ integrationIds }: WidgetProps<"downloads">) { - if (integrationIds.length === 0) { - return { - initialData: undefined, - }; - } - - const jobsAndStatuses = await api.widget.downloads.getJobsAndStatuses({ - integrationIds, - }); - - return { - initialData: jobsAndStatuses, - }; -} diff --git a/packages/widgets/src/health-monitoring/component.tsx b/packages/widgets/src/health-monitoring/component.tsx index 5bea4996a..dec33ff08 100644 --- a/packages/widgets/src/health-monitoring/component.tsx +++ b/packages/widgets/src/health-monitoring/component.tsx @@ -17,7 +17,7 @@ import { Text, Tooltip, } from "@mantine/core"; -import { useDisclosure, useElementSize, useListState } from "@mantine/hooks"; +import { useDisclosure, useElementSize } from "@mantine/hooks"; import { IconBrain, IconClock, @@ -30,19 +30,27 @@ import { IconVersions, } from "@tabler/icons-react"; +import { clientApi } from "@homarr/api/client"; import type { TranslationFunction } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { NoIntegrationSelectedError } from "../errors"; -export default function HealthMonitoringWidget({ - options, - integrationIds, - serverData, -}: WidgetComponentProps<"healthMonitoring">) { +export default function HealthMonitoringWidget({ options, integrationIds }: WidgetComponentProps<"healthMonitoring">) { const t = useI18n(); - const [healthData] = useListState(serverData?.initialData ?? []); + const [healthData] = clientApi.widget.healthMonitoring.getHealthStatus.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + select: (data) => data.filter((health) => health !== null), + }, + ); const [opened, { open, close }] = useDisclosure(false); if (integrationIds.length === 0) { diff --git a/packages/widgets/src/health-monitoring/index.ts b/packages/widgets/src/health-monitoring/index.ts index 3e7f9c548..5ad3c06cc 100644 --- a/packages/widgets/src/health-monitoring/index.ts +++ b/packages/widgets/src/health-monitoring/index.ts @@ -3,7 +3,7 @@ import { IconHeartRateMonitor, IconServerOff } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("healthMonitoring", { +export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { icon: IconHeartRateMonitor, options: optionsBuilder.from((factory) => ({ fahrenheit: factory.switch({ @@ -26,6 +26,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef message: (t) => t("widget.healthMonitoring.error.internalServerError"), }, }, -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/health-monitoring/serverData.ts b/packages/widgets/src/health-monitoring/serverData.ts deleted file mode 100644 index 3e68f587a..000000000 --- a/packages/widgets/src/health-monitoring/serverData.ts +++ /dev/null @@ -1,27 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../definition"; - -export default async function getServerDataAsync({ integrationIds }: WidgetProps<"healthMonitoring">) { - if (integrationIds.length === 0) { - return { - initialData: [], - }; - } - - try { - const currentHealthInfo = await api.widget.healthMonitoring.getHealthStatus({ - integrationIds, - }); - - return { - initialData: currentHealthInfo.filter((health) => health !== null), - }; - } catch { - return { - initialData: [], - }; - } -} diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index 645ba1df5..1f3050285 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -30,8 +30,6 @@ export { reduceWidgetOptionsWithDefaultValues } from "./options"; export type { WidgetDefinition } from "./definition"; export { WidgetEditModal } from "./modals/widget-edit-modal"; -export { useServerDataFor } from "./server/provider"; -export { GlobalItemServerDataRunner } from "./server/runner"; export type { WidgetComponentProps }; export const widgetImports = { diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index 6e487af5a..59b29bc7b 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -1,25 +1,26 @@ "use client"; -import { useState } from "react"; import { Anchor, Button, Card, Container, Flex, Group, ScrollArea, Text } from "@mantine/core"; import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; -import type { Indexer } from "@homarr/integrations/types"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { NoIntegrationSelectedError } from "../errors"; -export default function IndexerManagerWidget({ - options, - integrationIds, - serverData, -}: WidgetComponentProps<"indexerManager">) { +export default function IndexerManagerWidget({ options, integrationIds }: WidgetComponentProps<"indexerManager">) { const t = useI18n(); - const [indexersData, setIndexersData] = useState<{ integrationId: string; indexers: Indexer[] }[]>( - serverData?.initialData ?? [], + const [indexersData] = clientApi.widget.indexerManager.getIndexersStatus.useSuspenseQuery( + { integrationIds }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }, ); + const utils = clientApi.useUtils(); const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation(); @@ -27,11 +28,11 @@ export default function IndexerManagerWidget({ { integrationIds }, { onData(newData) { - setIndexersData((prevData) => { - return prevData.map((item) => + utils.widget.indexerManager.getIndexersStatus.setData({ integrationIds }, (previousData) => + previousData?.map((item) => item.integrationId === newData.integrationId ? { ...item, indexers: newData.indexers } : item, - ); - }); + ), + ); }, }, ); diff --git a/packages/widgets/src/indexer-manager/index.ts b/packages/widgets/src/indexer-manager/index.ts index 9885a1764..d0539194e 100644 --- a/packages/widgets/src/indexer-manager/index.ts +++ b/packages/widgets/src/indexer-manager/index.ts @@ -5,7 +5,7 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("indexerManager", { +export const { definition, componentLoader } = createWidgetDefinition("indexerManager", { icon: IconReportSearch, options: optionsBuilder.from((factory) => ({ openIndexerSiteInNewTab: factory.switch({ @@ -19,6 +19,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef message: (t) => t("widget.indexerManager.error.internalServerError"), }, }, -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/indexer-manager/serverData.ts b/packages/widgets/src/indexer-manager/serverData.ts deleted file mode 100644 index 4ec1c92d5..000000000 --- a/packages/widgets/src/indexer-manager/serverData.ts +++ /dev/null @@ -1,27 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../definition"; - -export default async function getServerDataAsync({ integrationIds }: WidgetProps<"indexerManager">) { - if (integrationIds.length === 0) { - return { - initialData: [], - }; - } - - try { - const currentIndexers = await api.widget.indexerManager.getIndexersStatus({ - integrationIds, - }); - - return { - initialData: currentIndexers, - }; - } catch { - return { - initialData: [], - }; - } -} diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 23208d209..bc6df6b25 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -15,30 +15,26 @@ export default function MediaServerWidget({ integrationIds, isEditMode, options, - serverData, itemId, }: WidgetComponentProps<"mediaRequests-requestList">) { const t = useScopedI18n("widget.mediaRequests-requestList"); - const isQueryEnabled = Boolean(itemId); - const { data: mediaRequests, isError: _isError } = clientApi.widget.mediaRequests.getLatestRequests.useQuery( + const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery( { integrationIds, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion itemId: itemId!, }, { - initialData: !serverData ? undefined : serverData.initialData, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, - enabled: integrationIds.length > 0 && isQueryEnabled, }, ); const sortedMediaRequests = useMemo( () => mediaRequests - ?.filter((group) => group != null) + .filter((group) => group != null) .flatMap((group) => group.data) .flatMap(({ medias, integration }) => medias.map((media) => ({ ...media, integrationId: integration.id }))) .sort(({ status: statusA }, { status: statusB }) => { @@ -49,7 +45,7 @@ export default function MediaServerWidget({ return 1; } return 0; - }) ?? [], + }), [mediaRequests, integrationIds], ); diff --git a/packages/widgets/src/media-requests/list/index.ts b/packages/widgets/src/media-requests/list/index.ts index 47ad762d9..93243e0df 100644 --- a/packages/widgets/src/media-requests/list/index.ts +++ b/packages/widgets/src/media-requests/list/index.ts @@ -5,7 +5,7 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createWidgetDefinition } from "../../definition"; import { optionsBuilder } from "../../options"; -export const { componentLoader, definition, serverDataLoader } = createWidgetDefinition("mediaRequests-requestList", { +export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", { icon: IconZoomQuestion, options: optionsBuilder.from((factory) => ({ linksTargetNewTab: factory.switch({ @@ -13,6 +13,4 @@ export const { componentLoader, definition, serverDataLoader } = createWidgetDef }), })), supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"), -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-requests/list/serverData.ts b/packages/widgets/src/media-requests/list/serverData.ts deleted file mode 100644 index 8784e4dd3..000000000 --- a/packages/widgets/src/media-requests/list/serverData.ts +++ /dev/null @@ -1,22 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../../definition"; - -export default async function getServerDataAsync({ integrationIds, itemId }: WidgetProps<"mediaRequests-requestList">) { - if (integrationIds.length === 0 || !itemId) { - return { - initialData: undefined, - }; - } - - const requests = await api.widget.mediaRequests.getLatestRequests({ - integrationIds, - itemId, - }); - - return { - initialData: requests.filter((group) => group != null), - }; -} diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index 75296856a..288bf1dc8 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -27,30 +27,26 @@ import classes from "./component.module.css"; export default function MediaServerWidget({ integrationIds, isEditMode, - serverData, itemId, }: WidgetComponentProps<"mediaRequests-requestStats">) { const t = useScopedI18n("widget.mediaRequests-requestStats"); - const isQueryEnabled = Boolean(itemId); - const { data: requestStats, isError: _isError } = clientApi.widget.mediaRequests.getStats.useQuery( + const [requestStats] = clientApi.widget.mediaRequests.getStats.useSuspenseQuery( { integrationIds, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion itemId: itemId!, }, { - initialData: !serverData ? undefined : serverData.initialData, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, - enabled: integrationIds.length > 0 && isQueryEnabled, }, ); const { width, height, ref } = useElementSize(); const baseData = useMemo( - () => requestStats?.filter((group) => group != null).flatMap((group) => group.data) ?? [], + () => requestStats.filter((group) => group != null).flatMap((group) => group.data), [requestStats], ); diff --git a/packages/widgets/src/media-requests/stats/index.ts b/packages/widgets/src/media-requests/stats/index.ts index 3d5576147..b8504c7d2 100644 --- a/packages/widgets/src/media-requests/stats/index.ts +++ b/packages/widgets/src/media-requests/stats/index.ts @@ -4,10 +4,8 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createWidgetDefinition } from "../../definition"; -export const { componentLoader, definition, serverDataLoader } = createWidgetDefinition("mediaRequests-requestStats", { +export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", { icon: IconChartBar, options: {}, supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"), -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-requests/stats/serverData.ts b/packages/widgets/src/media-requests/stats/serverData.ts deleted file mode 100644 index a534e0a6d..000000000 --- a/packages/widgets/src/media-requests/stats/serverData.ts +++ /dev/null @@ -1,25 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../../definition"; - -export default async function getServerDataAsync({ - integrationIds, - itemId, -}: WidgetProps<"mediaRequests-requestStats">) { - if (integrationIds.length === 0 || !itemId) { - return { - initialData: undefined, - }; - } - - const stats = await api.widget.mediaRequests.getStats({ - integrationIds, - itemId, - }); - - return { - initialData: stats.filter((group) => group != null), - }; -} diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index f70aa856c..b68b7f09c 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -2,7 +2,6 @@ import { useMemo } from "react"; import { Avatar, Box, Group, Text } from "@mantine/core"; -import { useListState } from "@mantine/hooks"; import type { MRT_ColumnDef } from "mantine-react-table"; import { MantineReactTable } from "mantine-react-table"; @@ -12,14 +11,19 @@ import { useTranslatedMantineReactTable } from "@homarr/ui/hooks"; import type { WidgetComponentProps } from "../definition"; -export default function MediaServerWidget({ - serverData, - integrationIds, - isEditMode, -}: WidgetComponentProps<"mediaServer">) { - const [currentStreams, currentStreamsHandlers] = useListState<{ integrationId: string; sessions: StreamSession[] }>( - serverData?.initialData ?? [], +export default function MediaServerWidget({ integrationIds, isEditMode }: WidgetComponentProps<"mediaServer">) { + const [currentStreams] = clientApi.widget.mediaServer.getCurrentStreams.useSuspenseQuery( + { + integrationIds, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }, ); + const utils = clientApi.useUtils(); + const columns = useMemo[]>( () => [ { @@ -62,15 +66,17 @@ export default function MediaServerWidget({ { enabled: !isEditMode, onData(data) { - currentStreamsHandlers.applyWhere( - (pair) => pair.integrationId === data.integrationId, - (pair) => { - return { - ...pair, - sessions: data.data, - }; - }, - ); + utils.widget.mediaServer.getCurrentStreams.setData({ integrationIds }, (previousData) => { + return previousData?.map((pair) => { + if (pair.integrationId === data.integrationId) { + return { + ...pair, + sessions: data.data, + }; + } + return pair; + }); + }); }, }, ); diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index f5efac742..e69919927 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -2,10 +2,8 @@ import { IconVideo } from "@tabler/icons-react"; import { createWidgetDefinition } from "../definition"; -export const { componentLoader, definition, serverDataLoader } = createWidgetDefinition("mediaServer", { +export const { componentLoader, definition } = createWidgetDefinition("mediaServer", { icon: IconVideo, options: {}, supportedIntegrations: ["jellyfin"], -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-server/serverData.ts b/packages/widgets/src/media-server/serverData.ts deleted file mode 100644 index 952cfadb4..000000000 --- a/packages/widgets/src/media-server/serverData.ts +++ /dev/null @@ -1,21 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../definition"; - -export default async function getServerDataAsync({ integrationIds }: WidgetProps<"mediaServer">) { - if (integrationIds.length === 0) { - return { - initialData: [], - }; - } - - const currentStreams = await api.widget.mediaServer.getCurrentStreams({ - integrationIds, - }); - - return { - initialData: currentStreams, - }; -} diff --git a/packages/widgets/src/rssFeed/component.tsx b/packages/widgets/src/rssFeed/component.tsx index 28d39b7bf..eb3780149 100644 --- a/packages/widgets/src/rssFeed/component.tsx +++ b/packages/widgets/src/rssFeed/component.tsx @@ -3,15 +3,29 @@ import { Card, Flex, Group, Image, ScrollArea, Stack, Text } from "@mantine/core import { IconClock } from "@tabler/icons-react"; import dayjs from "dayjs"; +import { clientApi } from "@homarr/api/client"; + import type { WidgetComponentProps } from "../definition"; import classes from "./component.module.scss"; -export default function RssFeed({ serverData, options }: WidgetComponentProps<"rssFeed">) { - if (serverData?.initialData === undefined) { - return null; - } +export default function RssFeed({ options, itemId }: WidgetComponentProps<"rssFeed">) { + const [rssFeeds] = clientApi.widget.rssFeed.getFeeds.useSuspenseQuery( + { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + itemId: itemId!, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + select(data) { + return data?.data ?? []; + }, + }, + ); - const entries = serverData.initialData + const entries = rssFeeds .filter((feedGroup) => feedGroup.feed.entries !== undefined) .flatMap((feedGroup) => feedGroup.feed.entries) .filter((entry) => entry !== undefined) diff --git a/packages/widgets/src/rssFeed/index.ts b/packages/widgets/src/rssFeed/index.ts index 6751bb859..5d61b2aeb 100644 --- a/packages/widgets/src/rssFeed/index.ts +++ b/packages/widgets/src/rssFeed/index.ts @@ -11,7 +11,7 @@ import { optionsBuilder } from "../options"; * - https://datatracker.ietf.org/doc/html/rfc5023 * - https://www.jsonfeed.org/version/1.1/ */ -export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("rssFeed", { +export const { definition, componentLoader } = createWidgetDefinition("rssFeed", { icon: IconRss, options: optionsBuilder.from((factory) => ({ feedUrls: factory.multiText({ @@ -27,6 +27,4 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef validate: z.number().min(1).max(9999), }), })), -}) - .withServerData(() => import("./serverData")) - .withDynamicImport(() => import("./component")); +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/rssFeed/serverData.ts b/packages/widgets/src/rssFeed/serverData.ts deleted file mode 100644 index a583a9141..000000000 --- a/packages/widgets/src/rssFeed/serverData.ts +++ /dev/null @@ -1,21 +0,0 @@ -"use server"; - -import { api } from "@homarr/api/server"; - -import type { WidgetProps } from "../definition"; - -export default async function getServerDataAsync({ itemId }: WidgetProps<"rssFeed">) { - if (!itemId) { - return { - initialData: undefined, - lastUpdatedAt: null, - }; - } - const data = await api.widget.rssFeed.getFeeds({ - itemId, - }); - return { - initialData: data?.data, - lastUpdatedAt: data?.timestamp, - }; -} diff --git a/packages/widgets/src/server/client.tsx b/packages/widgets/src/server/client.tsx deleted file mode 100644 index ceb43c313..000000000 --- a/packages/widgets/src/server/client.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { useServerDataInitializer } from "./provider"; - -interface Props { - id: string; - serverData: Record | undefined; -} - -export const ClientServerDataInitalizer = ({ id, serverData }: Props) => { - useServerDataInitializer(id, serverData); - return null; -}; diff --git a/packages/widgets/src/server/provider.tsx b/packages/widgets/src/server/provider.tsx deleted file mode 100644 index f2e745be0..000000000 --- a/packages/widgets/src/server/provider.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import type { PropsWithChildren } from "react"; -import { createContext, useContext, useEffect, useState } from "react"; - -type Data = Record< - string, - { - data: Record | undefined; - isReady: boolean; - } ->; - -interface GlobalItemServerDataContext { - setItemServerData: (id: string, data: Record | undefined) => void; - data: Data; - initalItemIds: string[]; -} - -const GlobalItemServerDataContext = createContext(null); - -interface Props { - initalItemIds: string[]; -} - -export const GlobalItemServerDataProvider = ({ children, initalItemIds }: PropsWithChildren) => { - const [data, setData] = useState({}); - - const setItemServerData = (id: string, itemData: Record | undefined) => { - setData((prev) => ({ - ...prev, - [id]: { - data: itemData, - isReady: true, - }, - })); - }; - - return ( - - {children} - - ); -}; - -export const useServerDataFor = (id: string) => { - const context = useContext(GlobalItemServerDataContext); - - if (!context) { - throw new Error("GlobalItemServerDataProvider is required"); - } - - // When the item is not in the initial list, it means the data can not come from the server - if (!context.initalItemIds.includes(id)) { - return { - data: undefined, - isReady: true, - }; - } - - return context.data[id]; -}; - -export const useServerDataInitializer = (id: string, serverData: Record | undefined) => { - const context = useContext(GlobalItemServerDataContext); - - if (!context) { - throw new Error("GlobalItemServerDataProvider is required"); - } - - useEffect(() => { - context.setItemServerData(id, serverData); - }, []); -}; diff --git a/packages/widgets/src/server/runner.tsx b/packages/widgets/src/server/runner.tsx deleted file mode 100644 index 91effec0f..000000000 --- a/packages/widgets/src/server/runner.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { PropsWithChildren } from "react"; -import { Suspense } from "react"; - -import type { RouterOutputs } from "@homarr/api"; - -import { reduceWidgetOptionsWithDefaultValues, widgetImports } from ".."; -import { ClientServerDataInitalizer } from "./client"; -import { GlobalItemServerDataProvider } from "./provider"; - -type Board = RouterOutputs["board"]["getHomeBoard"]; - -type Props = PropsWithChildren<{ - shouldRun: boolean; - board: Board; -}>; - -export const GlobalItemServerDataRunner = ({ board, shouldRun, children }: Props) => { - if (!shouldRun) return children; - - const allItems = board.sections.flatMap((section) => section.items); - - return ( - id)}> - {allItems.map((item) => ( - - - - ))} - {children} - - ); -}; - -interface ItemDataLoaderProps { - item: Board["sections"][number]["items"][number]; -} - -const ItemDataLoader = async ({ item }: ItemDataLoaderProps) => { - const widgetImport = widgetImports[item.kind]; - if (!("serverDataLoader" in widgetImport) || !widgetImport.serverDataLoader) { - return ; - } - const loader = await widgetImport.serverDataLoader(); - const optionsWithDefault = reduceWidgetOptionsWithDefaultValues(item.kind, item.options); - const data = await loader.default({ - ...item, - options: optionsWithDefault as never, - itemId: item.id, - }); - return ; -};