feat: modernize dashboard widgets
This commit is contained in:
1 parent
1a5e083fd0
commit
6f2cd7fc2b
270 files changed
+10189
-3544
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import type React from "react";
|
||||
import type { LoaderComponent } from "next/dynamic";
|
||||
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||
import { hashKey } from "@tanstack/react-query";
|
||||
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
|
||||
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
@@ -10,7 +11,7 @@ import type { stringOrTranslation } from "@homarr/translation";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { WidgetImports } from ".";
|
||||
import type { inferOptionsFromCreator, inferOptionsFromDefinition, WidgetOptionsRecord } from "./options";
|
||||
import type { inferOptionsFromCreator, WidgetOptionsRecord } from "./options";
|
||||
|
||||
export interface WidgetContextMenuAction {
|
||||
key: string;
|
||||
@@ -26,18 +27,50 @@ export interface WidgetContextMenuContext {
|
||||
isEditMode: boolean;
|
||||
boardId: string | undefined;
|
||||
itemId: string | undefined;
|
||||
canInteractWithSelectedIntegrations: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetContextActionProps<
|
||||
TKind extends WidgetKind,
|
||||
TOptions extends WidgetOptionsRecord = WidgetOptionsRecord,
|
||||
> {
|
||||
kind: TKind;
|
||||
options: inferOptionsFromDefinition<TOptions>;
|
||||
setOptions: (partial: Partial<inferOptionsFromDefinition<TOptions>>) => void;
|
||||
export interface WidgetRuntimeActions {
|
||||
togglePolling?: () => void;
|
||||
testAllIndexers?: () => void;
|
||||
previousPhoto?: () => void;
|
||||
nextPhoto?: () => void;
|
||||
toggleSlideshow?: () => void;
|
||||
}
|
||||
|
||||
export interface WidgetRuntimeState {
|
||||
queries: readonly NormalizedWidgetQuery[];
|
||||
actions: WidgetRuntimeActions;
|
||||
}
|
||||
|
||||
export type WidgetRuntimeRef = React.MutableRefObject<WidgetRuntimeState>;
|
||||
|
||||
export const createWidgetRuntimeState = (): WidgetRuntimeState => ({
|
||||
queries: [],
|
||||
actions: {},
|
||||
});
|
||||
|
||||
export interface NormalizedWidgetQuery {
|
||||
path: readonly string[];
|
||||
input: unknown;
|
||||
}
|
||||
|
||||
export interface WidgetQueryMatcherScope {
|
||||
itemId: string;
|
||||
boardId: string | undefined;
|
||||
integrationIds: readonly string[];
|
||||
options: Record<string, unknown>;
|
||||
runtimeQueries: readonly NormalizedWidgetQuery[];
|
||||
}
|
||||
|
||||
export type WidgetQueryMatcher = (query: NormalizedWidgetQuery, scope: WidgetQueryMatcherScope) => boolean;
|
||||
|
||||
export interface WidgetContextActionProps {
|
||||
options: Record<string, unknown>;
|
||||
setOptions: (partial: Record<string, unknown>) => void;
|
||||
integrationIds: string[];
|
||||
context: WidgetContextMenuContext;
|
||||
widgetStateRef: React.MutableRefObject<Record<string, unknown> | null>;
|
||||
widgetRuntimeRef: WidgetRuntimeRef;
|
||||
}
|
||||
|
||||
const createWithDynamicImport =
|
||||
@@ -69,8 +102,10 @@ export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition ext
|
||||
|
||||
export interface WidgetDefinition {
|
||||
icon: TablerIcon;
|
||||
supportsAdvancedFocus?: boolean;
|
||||
queryKey?: QueryKey;
|
||||
queryKeys?: QueryKey[];
|
||||
queryKeys?: readonly QueryKey[];
|
||||
queryMatcher?: WidgetQueryMatcher;
|
||||
refetchInterval?: number | null;
|
||||
supportedIntegrations?: IntegrationKind[];
|
||||
integrationsRequired?: boolean;
|
||||
@@ -88,11 +123,49 @@ export interface WidgetDefinition {
|
||||
}
|
||||
>
|
||||
>;
|
||||
contextActions?: (props: Omit<WidgetContextActionProps<WidgetKind>, "kind">) => WidgetContextMenuAction[];
|
||||
contextActions?: (props: WidgetContextActionProps) => WidgetContextMenuAction[];
|
||||
}
|
||||
|
||||
export const getWidgetQueryKeys = (definition: WidgetDefinition, kind: WidgetKind): QueryKey[] =>
|
||||
definition.queryKeys ?? [definition.queryKey ?? [["widget", kind]]];
|
||||
export const supportsAdvancedFocus = (definition: object) =>
|
||||
"supportsAdvancedFocus" in definition && definition.supportsAdvancedFocus === true;
|
||||
|
||||
export const getWidgetQueryKeys = (
|
||||
definition: {
|
||||
kind?: string;
|
||||
queryKey?: QueryKey;
|
||||
queryKeys?: readonly QueryKey[];
|
||||
},
|
||||
kind?: WidgetKind,
|
||||
): readonly QueryKey[] => {
|
||||
if (definition.queryKeys && definition.queryKeys.length > 0) return definition.queryKeys;
|
||||
return [definition.queryKey ?? [["widget", definition.kind ?? kind]]];
|
||||
};
|
||||
|
||||
export const normalizeWidgetQuery = (queryKey: QueryKey): NormalizedWidgetQuery | null => {
|
||||
const path = queryKey[0];
|
||||
if (!Array.isArray(path) || !path.every((part): part is string => typeof part === "string")) return null;
|
||||
|
||||
const queryKeyOptions = queryKey[1];
|
||||
const input = isRecord(queryKeyOptions) && "input" in queryKeyOptions ? queryKeyOptions.input : undefined;
|
||||
return { path, input };
|
||||
};
|
||||
|
||||
export const getWidgetRuntimeQueries = (widgetRuntimeRef: WidgetRuntimeRef): readonly NormalizedWidgetQuery[] =>
|
||||
widgetRuntimeRef.current.queries;
|
||||
|
||||
export const matchesWidgetRuntimeQuery: WidgetQueryMatcher = (query, scope) =>
|
||||
scope.runtimeQueries.some(
|
||||
(runtimeQuery) =>
|
||||
widgetQueryValueEquals(runtimeQuery.path, query.path) && widgetQueryValueEquals(runtimeQuery.input, query.input),
|
||||
);
|
||||
|
||||
export const widgetQueryInputMatches = (input: unknown, expected: Record<string, unknown>) =>
|
||||
isRecord(input) && Object.entries(expected).every(([key, value]) => widgetQueryValueEquals(input[key], value));
|
||||
|
||||
export const widgetQueryValueEquals = (left: unknown, right: unknown) => hashKey([left]) === hashKey([right]);
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
export interface WidgetProps<TKind extends WidgetKind> {
|
||||
options: inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>;
|
||||
@@ -103,9 +176,11 @@ export interface WidgetProps<TKind extends WidgetKind> {
|
||||
export type WidgetComponentProps<TKind extends WidgetKind> = WidgetProps<TKind> & {
|
||||
boardId: string | undefined; // undefined when in preview mode
|
||||
isEditMode: boolean;
|
||||
displayMode?: "compact" | "advanced";
|
||||
setOptions: ({ newOptions }: { newOptions: Partial<inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>> }) => void;
|
||||
width: number;
|
||||
height: number;
|
||||
widgetRuntimeRef?: WidgetRuntimeRef;
|
||||
widgetStateRef?: React.MutableRefObject<Record<string, unknown> | null>;
|
||||
removeItem?: () => void;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user