feat: add pi hole summary integration (#521)

* feat: add pi hole summary integration

* feat: add pi hole summary widget

* fix: type issues with integrations and integrationIds

* feat: add middleware for integrations and improve cache redis channel

* feat: add error boundary for widgets

* fix: broken lock file

* fix: format format issues

* fix: typecheck issue

* fix: deepsource issues

* fix: widget sandbox without error boundary

* chore: address pull request feedback

* chore: remove todo comment and created issue

* fix: format issues

* fix: deepsource issue
This commit is contained in:
Meier Lukas
2024-05-26 17:13:34 +02:00
committed by GitHub
parent 96c71aed6e
commit d57b771a17
45 changed files with 902 additions and 124 deletions

View File

@@ -0,0 +1,36 @@
import Link from "next/link";
import { Anchor, Button, Stack, Text } from "@mantine/core";
import type { stringOrTranslation } from "@homarr/translation";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
interface BaseWidgetErrorProps {
icon: TablerIcon;
message: stringOrTranslation;
showLogsLink?: boolean;
onRetry: () => void;
}
export const BaseWidgetError = (props: BaseWidgetErrorProps) => {
const t = useI18n();
return (
<Stack h="100%" align="center" justify="center" gap="md">
<props.icon size={40} />
<Stack gap={0}>
<Text ta="center">{translateIfNecessary(t, props.message)}</Text>
{props.showLogsLink && (
<Anchor component={Link} href="/manage/tools/logs" target="_blank" ta="center" size="sm">
{t("widget.common.error.action.logs")}
</Anchor>
)}
</Stack>
<Button onClick={props.onRetry} size="sm" variant="light">
{t("common.action.tryAgain")}
</Button>
</Stack>
);
};

View File

@@ -0,0 +1,11 @@
import type { TablerIcon } from "@tabler/icons-react";
import type { stringOrTranslation } from "@homarr/translation";
export abstract class ErrorBoundaryError extends Error {
public abstract getErrorBoundaryData(): {
icon: TablerIcon;
message: stringOrTranslation;
showLogsLink: boolean;
};
}

View File

@@ -0,0 +1,42 @@
import { useMemo } from "react";
import { IconExclamationCircle } from "@tabler/icons-react";
import { TRPCClientError } from "@trpc/client";
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
import type { WidgetKind } from "@homarr/definitions";
import { widgetImports } from "..";
import { ErrorBoundaryError } from "./base";
import { BaseWidgetError } from "./base-component";
interface WidgetErrorProps {
kind: WidgetKind;
error: unknown;
resetErrorBoundary: () => void;
}
export const WidgetError = ({ error, resetErrorBoundary, kind }: WidgetErrorProps) => {
const currentDefinition = useMemo(() => widgetImports[kind].definition, [kind]);
if (error instanceof ErrorBoundaryError) {
return <BaseWidgetError {...error.getErrorBoundaryData()} onRetry={resetErrorBoundary} />;
}
if (error instanceof TRPCClientError && "code" in error.data) {
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
icon={IconExclamationCircle}
message={(error as { toString: () => string }).toString()}
onRetry={resetErrorBoundary}
/>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./no-integration-selected";
export * from "./base";

View File

@@ -0,0 +1,19 @@
import { IconPlugX } from "@tabler/icons-react";
import type { TranslationFunction } from "@homarr/translation";
import { ErrorBoundaryError } from "./base";
export class NoIntegrationSelectedError extends ErrorBoundaryError {
constructor() {
super("No integration selected");
}
public getErrorBoundaryData() {
return {
icon: IconPlugX,
message: (t: TranslationFunction) => t("widget.common.error.noIntegration"),
showLogsLink: false,
};
}
}