refactor: use suspense query instead of serverdata for app widget (#1143)
* refactor: use suspense query instead of serverdata for app widget * chore: add missing translation for loading tooltip
This commit is contained in:
@@ -556,6 +556,7 @@ export default {
|
|||||||
next: "Next",
|
next: "Next",
|
||||||
checkoutDocs: "Check out the documentation",
|
checkoutDocs: "Check out the documentation",
|
||||||
tryAgain: "Try again",
|
tryAgain: "Try again",
|
||||||
|
loading: "Loading",
|
||||||
},
|
},
|
||||||
iconPicker: {
|
iconPicker: {
|
||||||
label: "Icon URL",
|
label: "Icon URL",
|
||||||
|
|||||||
@@ -1,58 +1,37 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { useState } from "react";
|
import { Suspense } from "react";
|
||||||
import { Box, Center, Flex, Loader, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
|
import { Flex, Text, Tooltip, UnstyledButton } from "@mantine/core";
|
||||||
import { IconDeviceDesktopX } from "@tabler/icons-react";
|
|
||||||
import combineClasses from "clsx";
|
import combineClasses from "clsx";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
||||||
import { useRegisterSpotlightActions } from "@homarr/spotlight";
|
import { useRegisterSpotlightActions } from "@homarr/spotlight";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
import classes from "./app.module.css";
|
import classes from "./app.module.css";
|
||||||
|
import { PingDot } from "./ping/ping-dot";
|
||||||
|
import { PingIndicator } from "./ping/ping-indicator";
|
||||||
|
|
||||||
export default function AppWidget({ options, serverData, isEditMode, width }: WidgetComponentProps<"app">) {
|
export default function AppWidget({ options, isEditMode }: WidgetComponentProps<"app">) {
|
||||||
const t = useScopedI18n("widget.app");
|
const t = useI18n();
|
||||||
const isQueryEnabled = Boolean(options.appId);
|
const [app] = clientApi.app.byId.useSuspenseQuery(
|
||||||
const {
|
|
||||||
data: app,
|
|
||||||
isPending,
|
|
||||||
isError,
|
|
||||||
} = clientApi.app.byId.useQuery(
|
|
||||||
{
|
{
|
||||||
id: options.appId,
|
id: options.appId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialData:
|
|
||||||
// We need to check if the id's match because otherwise the same initialData for a changed id will be used
|
|
||||||
serverData?.app?.id === options.appId ? serverData.app : undefined,
|
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
enabled: isQueryEnabled,
|
retry: false,
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [pingResult, setPingResult] = useState<RouterOutputs["widget"]["app"]["ping"] | null>(null);
|
|
||||||
|
|
||||||
const shouldRunPing = Boolean(app?.href) && options.pingEnabled;
|
|
||||||
clientApi.widget.app.updatedPing.useSubscription(
|
|
||||||
{ url: parseAppHrefWithVariablesClient(app?.href ?? "") },
|
|
||||||
{
|
|
||||||
enabled: shouldRunPing,
|
|
||||||
onData(data) {
|
|
||||||
setPingResult(data);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useRegisterSpotlightActions(
|
useRegisterSpotlightActions(
|
||||||
`app-${options.appId}`,
|
`app-${options.appId}`,
|
||||||
app?.href
|
app.href
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: `app-${options.appId}`,
|
id: `app-${options.appId}`,
|
||||||
@@ -69,44 +48,21 @@ export default function AppWidget({ options, serverData, isEditMode, width }: Wi
|
|||||||
[app, options.appId, options.openInNewTab],
|
[app, options.appId, options.openInNewTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isPending && isQueryEnabled) {
|
|
||||||
return (
|
|
||||||
<Center h="100%">
|
|
||||||
<Loader />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError || !isQueryEnabled) {
|
|
||||||
return (
|
|
||||||
<Tooltip.Floating label={t("error.notFound.tooltip")}>
|
|
||||||
<Stack gap="xs" align="center" justify="center" h="100%" w="100%">
|
|
||||||
<IconDeviceDesktopX size={width >= 96 ? "2rem" : "1rem"} />
|
|
||||||
{width >= 96 && (
|
|
||||||
<Text ta="center" size="sm">
|
|
||||||
{t("error.notFound.label")}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Tooltip.Floating>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLink
|
<AppLink
|
||||||
href={parseAppHrefWithVariablesClient(app?.href ?? "")}
|
href={parseAppHrefWithVariablesClient(app.href ?? "")}
|
||||||
openInNewTab={options.openInNewTab}
|
openInNewTab={options.openInNewTab}
|
||||||
enabled={Boolean(app?.href) && !isEditMode}
|
enabled={Boolean(app.href) && !isEditMode}
|
||||||
>
|
>
|
||||||
<Tooltip.Floating
|
<Tooltip.Floating
|
||||||
label={app?.description}
|
label={app.description}
|
||||||
position="right-start"
|
position="right-start"
|
||||||
multiline
|
multiline
|
||||||
disabled={!options.showDescriptionTooltip || !app?.description}
|
disabled={!options.showDescriptionTooltip || !app.description}
|
||||||
styles={{ tooltip: { maxWidth: 300 } }}
|
styles={{ tooltip: { maxWidth: 300 } }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
className={combineClasses("app-flex-wrapper", app?.name, app?.id)}
|
className={combineClasses("app-flex-wrapper", app.name, app.id)}
|
||||||
h="100%"
|
h="100%"
|
||||||
w="100%"
|
w="100%"
|
||||||
direction="column"
|
direction="column"
|
||||||
@@ -115,14 +71,22 @@ export default function AppWidget({ options, serverData, isEditMode, width }: Wi
|
|||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
{options.showTitle && (
|
{options.showTitle && (
|
||||||
<Text className="app-title" fw={700} size="12.5cqmin">
|
<Text className="app-title" fw={700} ta="center" size="12.5cqmin">
|
||||||
{app?.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<img src={app?.iconUrl} alt={app?.name} className={combineClasses(classes.appIcon, "app-icon")} />
|
<img src={app.iconUrl} alt={app.name} className={combineClasses(classes.appIcon, "app-icon")} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip.Floating>
|
</Tooltip.Floating>
|
||||||
{shouldRunPing && <PingIndicator pingResult={pingResult} />}
|
{options.pingEnabled && app.href ? (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<PingDot color="blue" tooltip={t("common.rtl", { symbol: "…", value: t("common.action.loading") })} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PingIndicator href={app.href} />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
</AppLink>
|
</AppLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -141,31 +105,3 @@ const AppLink = ({ href, openInNewTab, enabled, children }: PropsWithChildren<Ap
|
|||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
);
|
);
|
||||||
|
|
||||||
interface PingIndicatorProps {
|
|
||||||
pingResult: RouterOutputs["widget"]["app"]["ping"] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PingIndicator = ({ pingResult }: PingIndicatorProps) => {
|
|
||||||
return (
|
|
||||||
<Box bottom={4} right={4} pos="absolute">
|
|
||||||
<Tooltip
|
|
||||||
label={pingResult && "statusCode" in pingResult ? pingResult.statusCode : pingResult?.error}
|
|
||||||
disabled={!pingResult}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
borderRadius: "100%",
|
|
||||||
backgroundColor: !pingResult
|
|
||||||
? "orange"
|
|
||||||
: "error" in pingResult || pingResult.statusCode >= 500
|
|
||||||
? "red"
|
|
||||||
: "green",
|
|
||||||
}}
|
|
||||||
w={16}
|
|
||||||
h={16}
|
|
||||||
></Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IconApps } from "@tabler/icons-react";
|
import { IconApps, IconDeviceDesktopX } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
import { optionsBuilder } from "../options";
|
import { optionsBuilder } from "../options";
|
||||||
@@ -12,6 +12,11 @@ export const { definition, componentLoader, serverDataLoader } = createWidgetDef
|
|||||||
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
||||||
pingEnabled: factory.switch({ defaultValue: false }),
|
pingEnabled: factory.switch({ defaultValue: false }),
|
||||||
})),
|
})),
|
||||||
})
|
errors: {
|
||||||
.withServerData(() => import("./serverData"))
|
NOT_FOUND: {
|
||||||
.withDynamicImport(() => import("./component"));
|
icon: IconDeviceDesktopX,
|
||||||
|
message: (t) => t("widget.app.error.notFound.label"),
|
||||||
|
hideLogsLink: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).withDynamicImport(() => import("./component"));
|
||||||
|
|||||||
24
packages/widgets/src/app/ping/ping-dot.tsx
Normal file
24
packages/widgets/src/app/ping/ping-dot.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { MantineColor } from "@mantine/core";
|
||||||
|
import { Box, Tooltip } from "@mantine/core";
|
||||||
|
|
||||||
|
interface PingDotProps {
|
||||||
|
color: MantineColor;
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PingDot = ({ color, tooltip }: PingDotProps) => {
|
||||||
|
return (
|
||||||
|
<Box bottom="2.5cqmin" right="2.5cqmin" pos="absolute">
|
||||||
|
<Tooltip label={tooltip}>
|
||||||
|
<Box
|
||||||
|
bg={color}
|
||||||
|
style={{
|
||||||
|
borderRadius: "100%",
|
||||||
|
}}
|
||||||
|
w="10cqmin"
|
||||||
|
h="10cqmin"
|
||||||
|
></Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
packages/widgets/src/app/ping/ping-indicator.tsx
Normal file
41
packages/widgets/src/app/ping/ping-indicator.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
||||||
|
|
||||||
|
import { PingDot } from "./ping-dot";
|
||||||
|
|
||||||
|
interface PingIndicatorProps {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PingIndicator = ({ href }: PingIndicatorProps) => {
|
||||||
|
const [ping] = clientApi.widget.app.ping.useSuspenseQuery(
|
||||||
|
{
|
||||||
|
url: parseAppHrefWithVariablesClient(href),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [pingResult, setPingResult] = useState<RouterOutputs["widget"]["app"]["ping"]>(ping);
|
||||||
|
|
||||||
|
clientApi.widget.app.updatedPing.useSubscription(
|
||||||
|
{ url: parseAppHrefWithVariablesClient(href) },
|
||||||
|
{
|
||||||
|
onData(data) {
|
||||||
|
setPingResult(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PingDot
|
||||||
|
color={"error" in pingResult || pingResult.statusCode >= 500 ? "red" : "green"}
|
||||||
|
tooltip={"statusCode" in pingResult ? pingResult.statusCode.toString() : pingResult.error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
import { api } from "@homarr/api/server";
|
|
||||||
import { parseAppHrefWithVariablesServer } from "@homarr/common/server";
|
|
||||||
|
|
||||||
import type { WidgetProps } from "../definition";
|
|
||||||
|
|
||||||
export default async function getServerDataAsync({ options }: WidgetProps<"app">) {
|
|
||||||
if (!options.appId) {
|
|
||||||
return { app: null, pingResult: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const app = await api.app.byId({ id: options.appId });
|
|
||||||
let pingResult: RouterOutputs["widget"]["app"]["ping"] | null = null;
|
|
||||||
|
|
||||||
if (app.href && options.pingEnabled) {
|
|
||||||
pingResult = await api.widget.app.ping({
|
|
||||||
url: parseAppHrefWithVariablesServer(app.href),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { app, pingResult };
|
|
||||||
} catch {
|
|
||||||
return { app: null, pingResult: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { describe, expect, test, vi } from "vitest";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
import { api } from "@homarr/api/server";
|
|
||||||
import { objectKeys } from "@homarr/common";
|
|
||||||
|
|
||||||
import type { WidgetProps } from "../../definition";
|
|
||||||
import getServerDataAsync from "../serverData";
|
|
||||||
|
|
||||||
const mockApp = (override: Partial<RouterOutputs["app"]["byId"]>) =>
|
|
||||||
({
|
|
||||||
id: "1",
|
|
||||||
name: "Mock app",
|
|
||||||
iconUrl: "https://some.com/icon.png",
|
|
||||||
description: null,
|
|
||||||
href: "https://google.ch",
|
|
||||||
...override,
|
|
||||||
}) satisfies RouterOutputs["app"]["byId"];
|
|
||||||
|
|
||||||
vi.mock("@homarr/api/server", () => ({
|
|
||||||
api: {
|
|
||||||
app: {
|
|
||||||
byId: () => null,
|
|
||||||
},
|
|
||||||
widget: {
|
|
||||||
app: {
|
|
||||||
ping: () => null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
vi.mock("@homarr/common/server", () => ({
|
|
||||||
parseAppHrefWithVariablesServer: () => "http://localhost",
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("getServerDataAsync should load app and ping result", () => {
|
|
||||||
test("when appId is empty it should return null for app and pingResult", async () => {
|
|
||||||
// Arrange
|
|
||||||
const options = {
|
|
||||||
appId: "",
|
|
||||||
pingEnabled: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await getServerDataAsync({ options } as unknown as WidgetProps<"app">);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.app).toBeNull();
|
|
||||||
expect(result.pingResult).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when app exists and ping is disabled it should return existing app and pingResult null", async () => {
|
|
||||||
// Arrange
|
|
||||||
const spy = vi.spyOn(api.app, "byId");
|
|
||||||
const options = {
|
|
||||||
appId: "1",
|
|
||||||
pingEnabled: false,
|
|
||||||
};
|
|
||||||
const mockedApp = mockApp({});
|
|
||||||
spy.mockImplementation(() => Promise.resolve(mockedApp));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await getServerDataAsync({ options } as unknown as WidgetProps<"app">);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.pingResult).toBeNull();
|
|
||||||
objectKeys(mockedApp).forEach((key) => expect(result.app?.[key]).toBe(mockedApp[key]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when app exists without href and ping enabled it should return existing app and pingResult null", async () => {
|
|
||||||
// Arrange
|
|
||||||
const spy = vi.spyOn(api.app, "byId");
|
|
||||||
const options = {
|
|
||||||
appId: "1",
|
|
||||||
pingEnabled: true,
|
|
||||||
};
|
|
||||||
const mockedApp = mockApp({ href: null });
|
|
||||||
spy.mockImplementation(() => Promise.resolve(mockedApp));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await getServerDataAsync({ options } as unknown as WidgetProps<"app">);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.pingResult).toBeNull();
|
|
||||||
objectKeys(mockedApp).forEach((key) => expect(result.app?.[key]).toBe(mockedApp[key]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when app does not exist it should return for both null", async () => {
|
|
||||||
// Arrange
|
|
||||||
const spy = vi.spyOn(api.app, "byId");
|
|
||||||
const options = {
|
|
||||||
appId: "1",
|
|
||||||
pingEnabled: true,
|
|
||||||
};
|
|
||||||
spy.mockImplementation(() =>
|
|
||||||
Promise.reject(
|
|
||||||
new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await getServerDataAsync({ options } as unknown as WidgetProps<"app">);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.pingResult).toBeNull();
|
|
||||||
expect(result.app).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when app found and ping enabled it should return existing app and pingResult", async () => {
|
|
||||||
// Arrange
|
|
||||||
const spyById = vi.spyOn(api.app, "byId");
|
|
||||||
const spyPing = vi.spyOn(api.widget.app, "ping");
|
|
||||||
const options = {
|
|
||||||
appId: "1",
|
|
||||||
pingEnabled: true,
|
|
||||||
};
|
|
||||||
const mockedApp = mockApp({});
|
|
||||||
const pingResult = { statusCode: 200, url: "http://localhost" };
|
|
||||||
spyById.mockImplementation(() => Promise.resolve(mockedApp));
|
|
||||||
spyPing.mockImplementation(() => Promise.resolve(pingResult));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await getServerDataAsync({ options } as unknown as WidgetProps<"app">);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.pingResult).toBe(pingResult);
|
|
||||||
expect(result.app).toBe(mockedApp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -71,6 +71,7 @@ export interface WidgetDefinition {
|
|||||||
{
|
{
|
||||||
icon: TablerIcon;
|
icon: TablerIcon;
|
||||||
message: stringOrTranslation;
|
message: stringOrTranslation;
|
||||||
|
hideLogsLink?: boolean;
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import"
|
|||||||
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
|
||||||
|
import type { WidgetDefinition } from "..";
|
||||||
import { widgetImports } from "..";
|
import { widgetImports } from "..";
|
||||||
import { ErrorBoundaryError } from "./base";
|
import { ErrorBoundaryError } from "./base";
|
||||||
import { BaseWidgetError } from "./base-component";
|
import { BaseWidgetError } from "./base-component";
|
||||||
@@ -22,21 +23,28 @@ export const WidgetError = ({ error, resetErrorBoundary, kind }: WidgetErrorProp
|
|||||||
return <BaseWidgetError {...error.getErrorBoundaryData()} onRetry={resetErrorBoundary} />;
|
return <BaseWidgetError {...error.getErrorBoundaryData()} onRetry={resetErrorBoundary} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof TRPCClientError && "code" in error.data) {
|
const commonFallbackError = (
|
||||||
const errorData = error.data as DefaultErrorData;
|
|
||||||
|
|
||||||
if (!("errors" in currentDefinition && errorData.code in currentDefinition.errors)) return null;
|
|
||||||
|
|
||||||
const errorDefinition = currentDefinition.errors[errorData.code as keyof typeof currentDefinition.errors];
|
|
||||||
|
|
||||||
return <BaseWidgetError {...errorDefinition} onRetry={resetErrorBoundary} showLogsLink />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseWidgetError
|
<BaseWidgetError
|
||||||
icon={IconExclamationCircle}
|
icon={IconExclamationCircle}
|
||||||
message={(error as { toString: () => string }).toString()}
|
message={(error as { toString: () => string }).toString()}
|
||||||
onRetry={resetErrorBoundary}
|
onRetry={resetErrorBoundary}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (error instanceof TRPCClientError && "code" in error.data) {
|
||||||
|
const errorData = error.data as DefaultErrorData;
|
||||||
|
|
||||||
|
if (!("errors" in currentDefinition)) return commonFallbackError;
|
||||||
|
|
||||||
|
const errors: Exclude<WidgetDefinition["errors"], undefined> = currentDefinition.errors;
|
||||||
|
const errorDefinition = errors[errorData.code];
|
||||||
|
|
||||||
|
if (!errorDefinition) return commonFallbackError;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseWidgetError {...errorDefinition} onRetry={resetErrorBoundary} showLogsLink={!errorDefinition.hideLogsLink} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonFallbackError;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import type { Loader } from "next/dynamic";
|
import type { Loader } from "next/dynamic";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Loader as UiLoader } from "@mantine/core";
|
import { Center, Loader as UiLoader } from "@mantine/core";
|
||||||
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
|
||||||
@@ -65,7 +65,11 @@ export const loadWidgetDynamic = <TKind extends WidgetKind>(kind: TKind) => {
|
|||||||
const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>(
|
const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>(
|
||||||
widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>,
|
widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>,
|
||||||
{
|
{
|
||||||
loading: () => <UiLoader />,
|
loading: () => (
|
||||||
|
<Center w="100%" h="100%">
|
||||||
|
<UiLoader />
|
||||||
|
</Center>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ interface ItemDataLoaderProps {
|
|||||||
|
|
||||||
const ItemDataLoader = async ({ item }: ItemDataLoaderProps) => {
|
const ItemDataLoader = async ({ item }: ItemDataLoaderProps) => {
|
||||||
const widgetImport = widgetImports[item.kind];
|
const widgetImport = widgetImports[item.kind];
|
||||||
if (!("serverDataLoader" in widgetImport)) {
|
if (!("serverDataLoader" in widgetImport) || !widgetImport.serverDataLoader) {
|
||||||
return <ClientServerDataInitalizer id={item.id} serverData={undefined} />;
|
return <ClientServerDataInitalizer id={item.id} serverData={undefined} />;
|
||||||
}
|
}
|
||||||
const loader = await widgetImport.serverDataLoader();
|
const loader = await widgetImport.serverDataLoader();
|
||||||
|
|||||||
Reference in New Issue
Block a user