docs: add varied improvements (#5942)
This commit is contained in:
1 parent
8598159894
commit
93c008ff4f
29 files changed
+719
-383
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Alert, Anchor, Text } from "@mantine/core";
|
||||
import { IconFlask } from "@tabler/icons-react";
|
||||
|
||||
const FEEDBACK_URL = "https://github.com/homarr-labs/homarr/discussions/5916";
|
||||
const FEEDBACK_URL = "https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets";
|
||||
|
||||
export function CustomWidgetBetaBanner() {
|
||||
return (
|
||||
|
||||
@@ -150,7 +150,13 @@ const requiredFieldValidators: Record<string, (data: Record<string, unknown>, ct
|
||||
});
|
||||
},
|
||||
statusIndicator: (data, ctx) => {
|
||||
(data.statusItems as Array<{ label: string; jsonPath: string; goodValues: string }>)?.forEach((item, i) => {
|
||||
(
|
||||
data.statusItems as Array<{
|
||||
label: string;
|
||||
jsonPath: string;
|
||||
goodValues: string;
|
||||
}>
|
||||
)?.forEach((item, i) => {
|
||||
if (!item.label)
|
||||
ctx.addIssue({
|
||||
code: "too_small",
|
||||
@@ -260,15 +266,34 @@ const formSchema = z
|
||||
columns: z.array(z.object({ header: z.string(), jsonPath: z.string() })),
|
||||
striped: z.boolean(),
|
||||
compact: z.boolean(),
|
||||
statGridItems: z.array(z.object({ label: z.string(), jsonPath: z.string(), unit: z.string(), color: z.string() })),
|
||||
statGridItems: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
jsonPath: z.string(),
|
||||
unit: z.string(),
|
||||
color: z.string(),
|
||||
}),
|
||||
),
|
||||
statGridColumns: z.number(),
|
||||
cardStyle: z.string(),
|
||||
progressBars: z.array(
|
||||
z.object({ label: z.string(), valuePath: z.string(), maxPath: z.string(), unit: z.string(), color: z.string() }),
|
||||
z.object({
|
||||
label: z.string(),
|
||||
valuePath: z.string(),
|
||||
maxPath: z.string(),
|
||||
unit: z.string(),
|
||||
color: z.string(),
|
||||
}),
|
||||
),
|
||||
showPercentage: z.boolean(),
|
||||
barSize: z.string(),
|
||||
statusItems: z.array(z.object({ label: z.string(), jsonPath: z.string(), goodValues: z.string() })),
|
||||
statusItems: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
jsonPath: z.string(),
|
||||
goodValues: z.string(),
|
||||
}),
|
||||
),
|
||||
statusLayout: z.string(),
|
||||
dotSize: z.string(),
|
||||
countGridItems: z.array(z.object({ label: z.string(), jsonPath: z.string(), unit: z.string() })),
|
||||
@@ -281,7 +306,13 @@ const formSchema = z
|
||||
confirmText: z.string(),
|
||||
successMessage: z.string(),
|
||||
template: z.string(),
|
||||
secrets: z.array(z.object({ kind: z.string(), value: z.string(), hasValue: z.boolean().optional() })),
|
||||
secrets: z.array(
|
||||
z.object({
|
||||
kind: z.string(),
|
||||
value: z.string(),
|
||||
hasValue: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const validator = requiredFieldValidators[data.displayType];
|
||||
@@ -362,12 +393,31 @@ const displayConfigBuilders: Record<string, (values: z.infer<typeof formSchema>)
|
||||
valueSize: v.valueSize,
|
||||
labelPosition: v.labelPosition,
|
||||
}),
|
||||
keyValue: (v) => ({ type: "keyValue", mappings: v.mappings, layout: v.kvLayout, columns: v.kvColumns }),
|
||||
table: (v) => ({ type: "table", tablePath: v.tablePath, columns: v.columns, striped: v.striped, compact: v.compact }),
|
||||
statGrid: (v) => ({ type: "statGrid", items: v.statGridItems, columns: v.statGridColumns, cardStyle: v.cardStyle }),
|
||||
keyValue: (v) => ({
|
||||
type: "keyValue",
|
||||
mappings: v.mappings,
|
||||
layout: v.kvLayout,
|
||||
columns: v.kvColumns,
|
||||
}),
|
||||
table: (v) => ({
|
||||
type: "table",
|
||||
tablePath: v.tablePath,
|
||||
columns: v.columns,
|
||||
striped: v.striped,
|
||||
compact: v.compact,
|
||||
}),
|
||||
statGrid: (v) => ({
|
||||
type: "statGrid",
|
||||
items: v.statGridItems,
|
||||
columns: v.statGridColumns,
|
||||
cardStyle: v.cardStyle,
|
||||
}),
|
||||
progressBars: (v) => ({
|
||||
type: "progressBars",
|
||||
bars: v.progressBars.map((b) => ({ ...b, maxPath: b.maxPath || undefined })),
|
||||
bars: v.progressBars.map((b) => ({
|
||||
...b,
|
||||
maxPath: b.maxPath || undefined,
|
||||
})),
|
||||
showPercentage: v.showPercentage,
|
||||
barSize: v.barSize,
|
||||
}),
|
||||
@@ -389,7 +439,11 @@ const displayConfigBuilders: Record<string, (values: z.infer<typeof formSchema>)
|
||||
columns: v.countGridColumns,
|
||||
valueSize: v.countValueSize,
|
||||
}),
|
||||
raw: (v) => ({ type: "raw", jsonPath: v.rawJsonPath, maxHeight: v.rawMaxHeight }),
|
||||
raw: (v) => ({
|
||||
type: "raw",
|
||||
jsonPath: v.rawJsonPath,
|
||||
maxHeight: v.rawMaxHeight,
|
||||
}),
|
||||
actionButton: (v) => ({
|
||||
type: "actionButton",
|
||||
buttonLabel: v.buttonLabel,
|
||||
@@ -415,7 +469,10 @@ const serverToFormFieldMap: Record<string, Record<string, string>> = {
|
||||
|
||||
function extractServerErrors(err: unknown, displayType: string): Record<string, string> {
|
||||
const errors: Record<string, string> = {};
|
||||
const trpcErr = err as { data?: { zodError?: { fieldErrors?: Record<string, string[]> } }; message?: string };
|
||||
const trpcErr = err as {
|
||||
data?: { zodError?: { fieldErrors?: Record<string, string[]> } };
|
||||
message?: string;
|
||||
};
|
||||
|
||||
if (trpcErr?.data?.zodError?.fieldErrors) {
|
||||
for (const [field, messages] of Object.entries(trpcErr.data.zodError.fieldErrors)) {
|
||||
@@ -427,7 +484,10 @@ function extractServerErrors(err: unknown, displayType: string): Record<string,
|
||||
}
|
||||
|
||||
try {
|
||||
const issues = JSON.parse(trpcErr?.message ?? "[]") as Array<{ path: (string | number)[]; message: string }>;
|
||||
const issues = JSON.parse(trpcErr?.message ?? "[]") as Array<{
|
||||
path: (string | number)[];
|
||||
message: string;
|
||||
}>;
|
||||
const fieldMap = serverToFormFieldMap[displayType] ?? {};
|
||||
|
||||
for (const issue of issues) {
|
||||
@@ -451,7 +511,13 @@ const listItemDefaults = {
|
||||
mapping: { label: "", jsonPath: "$", unit: "" },
|
||||
column: { header: "", jsonPath: "$" },
|
||||
statGridItem: { label: "", jsonPath: "$", unit: "", color: "blue" },
|
||||
progressBar: { label: "", valuePath: "$", maxPath: "", unit: "", color: "blue" },
|
||||
progressBar: {
|
||||
label: "",
|
||||
valuePath: "$",
|
||||
maxPath: "",
|
||||
unit: "",
|
||||
color: "blue",
|
||||
},
|
||||
statusItem: { label: "", jsonPath: "$", goodValues: "online,true" },
|
||||
countGridItem: { label: "", jsonPath: "$", unit: "" },
|
||||
} as const;
|
||||
@@ -521,7 +587,10 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
displayConfig: displayConfig as never,
|
||||
secrets: values.secrets
|
||||
.filter((s) => s.value)
|
||||
.map((s) => ({ kind: s.kind as "apiKey" | "username" | "password", value: s.value })),
|
||||
.map((s) => ({
|
||||
kind: s.kind as "apiKey" | "username" | "password",
|
||||
value: s.value,
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -535,7 +604,10 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
router.push(`/manage/custom-widgets/edit/${result.id}`);
|
||||
} else if (definitionId) {
|
||||
await updateMutation.mutateAsync({ id: definitionId, ...payload });
|
||||
showSuccessNotification({ title: t("action.save"), message: t("notification.updated", { name: values.name }) });
|
||||
showSuccessNotification({
|
||||
title: t("action.save"),
|
||||
message: t("notification.updated", { name: values.name }),
|
||||
});
|
||||
setPreviewRefreshSignal((n) => n + 1);
|
||||
await utils.customWidget.all.invalidate();
|
||||
await utils.customWidget.byId.invalidate({ id: definitionId });
|
||||
@@ -547,7 +619,10 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
form.setErrors(serverErrors);
|
||||
}
|
||||
const errorKey = mode === "create" ? "notification.createError" : "notification.updateError";
|
||||
showErrorNotification({ title: t("action.save"), message: t(errorKey as never) });
|
||||
showErrorNotification({
|
||||
title: t("action.save"),
|
||||
message: t(errorKey as never),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -566,7 +641,10 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
displayConfig: buildConfig?.(values) as z.infer<typeof displayConfigSchema>,
|
||||
secrets: values.secrets
|
||||
.filter((s) => s.value)
|
||||
.map((s) => ({ kind: s.kind as CustomWidgetSecretKind, value: s.value })),
|
||||
.map((s) => ({
|
||||
kind: s.kind as CustomWidgetSecretKind,
|
||||
value: s.value,
|
||||
})),
|
||||
definitionId,
|
||||
};
|
||||
}, [form.values, definitionId]);
|
||||
@@ -749,7 +827,18 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Box w={340} style={{ flexShrink: 0, position: "sticky", top: 100, alignSelf: "start" }} visibleFrom="md">
|
||||
<Box
|
||||
w={480}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
position: "sticky",
|
||||
top: 80,
|
||||
alignSelf: "start",
|
||||
maxHeight: "calc(100vh - 100px)",
|
||||
overflow: "auto",
|
||||
}}
|
||||
visibleFrom="md"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Button type="submit" fullWidth loading={createMutation.isPending || updateMutation.isPending}>
|
||||
{mode === "create" ? t("action.create") : t("action.save")}
|
||||
@@ -817,13 +906,19 @@ function DisplayTypeFields({
|
||||
<Group grow>
|
||||
<Select
|
||||
label={t("field.valueSize")}
|
||||
data={["sm", "md", "lg", "xl"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
|
||||
data={["sm", "md", "lg", "xl"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`sizeOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("valueSize")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
<Select
|
||||
label={t("field.labelPosition")}
|
||||
data={["above", "below"].map((v) => ({ value: v, label: t(`labelPositionOption.${v}` as never) }))}
|
||||
data={["above", "below"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`labelPositionOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("labelPosition")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
@@ -838,7 +933,10 @@ function DisplayTypeFields({
|
||||
<Group grow>
|
||||
<Select
|
||||
label={t("field.kvLayout")}
|
||||
data={["list", "grid"].map((v) => ({ value: v, label: t(`layoutOption.${v}` as never) }))}
|
||||
data={["list", "grid"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`layoutOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("kvLayout")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
@@ -958,7 +1056,10 @@ function DisplayTypeFields({
|
||||
<NumberInput label={t("field.gridColumns")} min={1} max={4} {...form.getInputProps("statGridColumns")} />
|
||||
<Select
|
||||
label={t("field.cardStyle")}
|
||||
data={["filled", "outline", "subtle"].map((v) => ({ value: v, label: t(`cardStyleOption.${v}` as never) }))}
|
||||
data={["filled", "outline", "subtle"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`cardStyleOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("cardStyle")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
@@ -1027,7 +1128,10 @@ function DisplayTypeFields({
|
||||
<Switch label={t("field.showPercentage")} {...form.getInputProps("showPercentage", { type: "checkbox" })} />
|
||||
<Select
|
||||
label={t("field.barSize")}
|
||||
data={["sm", "md", "lg"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
|
||||
data={["sm", "md", "lg"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`sizeOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("barSize")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
@@ -1103,13 +1207,19 @@ function DisplayTypeFields({
|
||||
<Group grow>
|
||||
<Select
|
||||
label={t("field.kvLayout")}
|
||||
data={["list", "grid"].map((v) => ({ value: v, label: t(`layoutOption.${v}` as never) }))}
|
||||
data={["list", "grid"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`layoutOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("statusLayout")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
<Select
|
||||
label={t("field.dotSize")}
|
||||
data={["sm", "md", "lg"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
|
||||
data={["sm", "md", "lg"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`sizeOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("dotSize")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
@@ -1171,7 +1281,10 @@ function DisplayTypeFields({
|
||||
<NumberInput label={t("field.gridColumns")} min={2} max={4} {...form.getInputProps("countGridColumns")} />
|
||||
<Select
|
||||
label={t("field.valueSize")}
|
||||
data={["sm", "md", "lg"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
|
||||
data={["sm", "md", "lg"].map((v) => ({
|
||||
value: v,
|
||||
label: t(`sizeOption.${v}` as never),
|
||||
}))}
|
||||
{...form.getInputProps("countValueSize")}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
|
||||
@@ -109,7 +109,7 @@ export function CustomWidgetPreview({
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Card withBorder p="xs" bg="var(--mantine-color-dark-7)" mih={120}>
|
||||
<Card withBorder p="xs" bg="var(--mantine-color-dark-7)" mah={320} style={{ overflow: "auto" }}>
|
||||
<PreviewDisplay data={displayData} />
|
||||
</Card>
|
||||
|
||||
@@ -119,7 +119,9 @@ export function CustomWidgetPreview({
|
||||
variant="subtle"
|
||||
leftSection={<IconExternalLink size={14} />}
|
||||
onClick={() => {
|
||||
const blob = new Blob([fetchResult.rawResponse ?? ""], { type: "application/json" });
|
||||
const blob = new Blob([fetchResult.rawResponse ?? ""], {
|
||||
type: "application/json",
|
||||
});
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
window.open(blobUrl);
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
||||
|
||||
@@ -163,7 +163,7 @@ export default async function EditCustomWidgetPage(props: EditCustomWidgetPagePr
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb dynamicMappings={new Map([[params.id, definition.name]])} nonInteractable={["edit"]} />
|
||||
<Container size="xl">
|
||||
<Container fluid>
|
||||
<Stack>
|
||||
<Title>{definition.name}</Title>
|
||||
<CustomWidgetBetaBanner />
|
||||
|
||||
@@ -19,7 +19,7 @@ export default async function NewCustomWidgetPage() {
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Container size="xl">
|
||||
<Container fluid>
|
||||
<Stack>
|
||||
<div>
|
||||
<Title>{t("page.create.title")}</Title>
|
||||
|
||||
Reference in new issue
Block a user