Replace plain text systemId option with a dedicated beszelSystem factory type backed by a searchable Select that fetches available systems from the integration. Shows proper states for no integration, loading, and no systems found.
254 lines
8.3 KiB
TypeScript
254 lines
8.3 KiB
TypeScript
import type React from "react";
|
|
import type { DraggableAttributes, UniqueIdentifier } from "@dnd-kit/core";
|
|
import type { ActionIconProps } from "@mantine/core";
|
|
import { z } from "zod/v4";
|
|
import type { ZodType } from "zod/v4";
|
|
|
|
import type { IntegrationKind } from "@homarr/definitions";
|
|
|
|
import type { DynamicSelectOption } from "./_inputs/widget-dynamic-select-input";
|
|
import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input";
|
|
import type { ReleasesRepository } from "./releases/releases-repository";
|
|
|
|
interface CommonInput<TType> {
|
|
defaultValue?: TType;
|
|
withDescription?: boolean;
|
|
}
|
|
|
|
interface TextInput extends CommonInput<string> {
|
|
validate?: z.ZodType<string>;
|
|
}
|
|
|
|
type AnchorNoteInput = CommonInput<string>;
|
|
|
|
interface MultiSelectInput<TOptions extends SelectOption[]> extends CommonInput<
|
|
inferSelectOptionValue<TOptions[number]>[]
|
|
> {
|
|
options: TOptions;
|
|
searchable?: boolean;
|
|
}
|
|
|
|
export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier> extends Omit<
|
|
CommonInput<TOptionValue[]>,
|
|
"withDescription"
|
|
> {
|
|
AddButton: (props: { addItem: (item: TItem) => void; values: TOptionValue[] }) => React.ReactNode;
|
|
ItemComponent: (props: {
|
|
item: TItem;
|
|
removeItem: () => void;
|
|
rootAttributes: DraggableAttributes;
|
|
handle: (props: Partial<Pick<ActionIconProps, "size" | "color" | "variant">>) => React.ReactNode;
|
|
}) => React.ReactNode;
|
|
uniqueIdentifier: (item: TItem) => TOptionValue;
|
|
useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown };
|
|
}
|
|
|
|
interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput<
|
|
inferSelectOptionValue<TOptions[number]>
|
|
> {
|
|
options: TOptions;
|
|
searchable?: boolean;
|
|
}
|
|
|
|
interface DynamicSelectInput extends CommonInput<DynamicSelectOption | null> {
|
|
useOptions: (
|
|
query: string,
|
|
integrationIds: string[],
|
|
) => {
|
|
isPending: boolean;
|
|
options: DynamicSelectOption[];
|
|
};
|
|
}
|
|
|
|
interface NumberInput extends CommonInput<number> {
|
|
validate: z.ZodNumber;
|
|
step?: number;
|
|
}
|
|
|
|
interface SliderInput extends CommonInput<number> {
|
|
validate: z.ZodNumber;
|
|
step?: number;
|
|
}
|
|
|
|
export interface OptionLocation {
|
|
name: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
}
|
|
|
|
const optionsFactory = {
|
|
switch: (input?: CommonInput<boolean>) => ({
|
|
type: "switch" as const,
|
|
defaultValue: input?.defaultValue ?? false,
|
|
withDescription: input?.withDescription ?? false,
|
|
}),
|
|
text: (input?: TextInput) => ({
|
|
type: "text" as const,
|
|
defaultValue: input?.defaultValue ?? "",
|
|
withDescription: input?.withDescription ?? false,
|
|
validate: input?.validate,
|
|
}),
|
|
anchorNote: (input?: AnchorNoteInput) => ({
|
|
type: "anchorNote" as const,
|
|
defaultValue: input?.defaultValue ?? "",
|
|
withDescription: input?.withDescription ?? false,
|
|
}),
|
|
multiSelect: <const TOptions extends SelectOption[]>(input: MultiSelectInput<TOptions>) => ({
|
|
type: "multiSelect" as const,
|
|
defaultValue: input.defaultValue ?? [],
|
|
options: input.options,
|
|
searchable: input.searchable ?? false,
|
|
withDescription: input.withDescription ?? false,
|
|
}),
|
|
dynamicSelect: (input: DynamicSelectInput) => ({
|
|
type: "dynamicSelect" as const,
|
|
defaultValue: input.defaultValue ?? null,
|
|
useOptions: input.useOptions,
|
|
withDescription: input.withDescription ?? false,
|
|
}),
|
|
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
|
|
type: "select" as const,
|
|
defaultValue: (input.defaultValue ?? input.options[0]) as inferSelectOptionValue<TOptions[number]>,
|
|
options: input.options,
|
|
searchable: input.searchable ?? false,
|
|
withDescription: input.withDescription ?? false,
|
|
}),
|
|
number: (input: NumberInput) => ({
|
|
type: "number" as const,
|
|
defaultValue: input.defaultValue ?? 0,
|
|
step: input.step,
|
|
withDescription: input.withDescription ?? false,
|
|
validate: input.validate,
|
|
}),
|
|
slider: (input: SliderInput) => ({
|
|
type: "slider" as const,
|
|
defaultValue: input.defaultValue ?? input.validate.minValue ?? 0,
|
|
step: input.step,
|
|
withDescription: input.withDescription ?? false,
|
|
validate: input.validate,
|
|
}),
|
|
location: (input?: CommonInput<OptionLocation>) => ({
|
|
type: "location" as const,
|
|
defaultValue: input?.defaultValue ?? {
|
|
name: "",
|
|
latitude: 0,
|
|
longitude: 0,
|
|
},
|
|
withDescription: input?.withDescription ?? false,
|
|
validate: z.object({
|
|
name: z.string().min(1),
|
|
latitude: z.number(),
|
|
longitude: z.number(),
|
|
}),
|
|
}),
|
|
multiText: (input?: CommonInput<string[]> & { validate?: ZodType }) => ({
|
|
type: "multiText" as const,
|
|
defaultValue: input?.defaultValue ?? [],
|
|
withDescription: input?.withDescription ?? false,
|
|
values: [] as string[],
|
|
validate: input?.validate,
|
|
}),
|
|
multiReleasesRepositories: (input?: CommonInput<ReleasesRepository[]> & { validate?: ZodType }) => ({
|
|
type: "multiReleasesRepositories" as const,
|
|
defaultValue: input?.defaultValue ?? [],
|
|
withDescription: input?.withDescription ?? false,
|
|
values: [] as ReleasesRepository[],
|
|
validate: input?.validate,
|
|
}),
|
|
app: () => ({
|
|
type: "app" as const,
|
|
defaultValue: "",
|
|
withDescription: false,
|
|
}),
|
|
umamiEventName: () => ({
|
|
type: "umamiEventName" as const,
|
|
defaultValue: "",
|
|
withDescription: true,
|
|
}),
|
|
umamiEventNames: () => ({
|
|
type: "umamiEventNames" as const,
|
|
defaultValue: [] as string[],
|
|
withDescription: true,
|
|
}),
|
|
umamiWebsite: () => ({
|
|
type: "umamiWebsite" as const,
|
|
defaultValue: "",
|
|
withDescription: true,
|
|
}),
|
|
beszelSystem: () => ({
|
|
type: "beszelSystem" as const,
|
|
defaultValue: "",
|
|
withDescription: true,
|
|
}),
|
|
customWidgetSelect: (input?: CommonInput<string>) => ({
|
|
type: "customWidgetSelect" as const,
|
|
defaultValue: input?.defaultValue ?? "",
|
|
withDescription: input?.withDescription ?? false,
|
|
}),
|
|
sortableItemList: <const TItem, const TOptionValue extends UniqueIdentifier>(
|
|
input: SortableItemListInput<TItem, TOptionValue>,
|
|
) => ({
|
|
type: "sortableItemList" as const,
|
|
defaultValue: [] as TOptionValue[],
|
|
itemComponent: input.ItemComponent,
|
|
addButton: input.AddButton,
|
|
uniqueIdentifier: input.uniqueIdentifier,
|
|
useData: input.useData,
|
|
withDescription: false,
|
|
}),
|
|
};
|
|
|
|
type WidgetOptionFactory = typeof optionsFactory;
|
|
|
|
export type WidgetOptionDefinition =
|
|
| ReturnType<WidgetOptionFactory[Exclude<keyof WidgetOptionFactory, "sortableItemList">]>
|
|
// We allow any here as it's already type guarded with Record<string, unknown> and it still infers the correct type
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
| ReturnType<typeof optionsFactory.sortableItemList<any, any>>;
|
|
export type WidgetOptionsRecord = Record<string, WidgetOptionDefinition>;
|
|
export type WidgetOptionType = WidgetOptionDefinition["type"];
|
|
export type WidgetOptionOfType<TType extends WidgetOptionType> = Extract<WidgetOptionDefinition, { type: TType }>;
|
|
|
|
type inferOptionFromDefinition<TDefinition extends WidgetOptionDefinition> = TDefinition["defaultValue"];
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export type inferOptionsFromCreator<TOptions extends (settings: any) => WidgetOptionsRecord> =
|
|
inferOptionsFromDefinition<ReturnType<TOptions>>;
|
|
export type inferOptionsFromDefinition<TOptions extends WidgetOptionsRecord> = {
|
|
[key in keyof TOptions]: inferOptionFromDefinition<TOptions[key]>;
|
|
};
|
|
|
|
interface FieldConfiguration<TOptions extends WidgetOptionsRecord> {
|
|
shouldHide: (options: inferOptionsFromDefinition<TOptions>, integrationKinds: IntegrationKind[]) => boolean;
|
|
}
|
|
|
|
type ConfigurationInput<TOptions extends WidgetOptionsRecord> = Partial<
|
|
Record<keyof TOptions, FieldConfiguration<TOptions>>
|
|
>;
|
|
|
|
const createOptions = <TOptions extends WidgetOptionsRecord>(
|
|
optionsCallback: (factory: WidgetOptionFactory) => TOptions,
|
|
configuration?: ConfigurationInput<TOptions>,
|
|
) => {
|
|
const obj = {} as Record<keyof TOptions, unknown>;
|
|
const options = optionsCallback(optionsFactory);
|
|
|
|
for (const key in options) {
|
|
obj[key] = {
|
|
...configuration?.[key],
|
|
...options[key],
|
|
};
|
|
}
|
|
|
|
return obj as {
|
|
[key in keyof TOptions]: TOptions[key] & FieldConfiguration<TOptions>;
|
|
};
|
|
};
|
|
|
|
type OptionsBuilder = typeof createOptions;
|
|
export type OptionsBuilderResult = ReturnType<OptionsBuilder>;
|
|
|
|
export const optionsBuilder = {
|
|
from: createOptions,
|
|
};
|