fix: complete widget review cleanup

This commit is contained in:
Thomas Camlong committed 2026-08-02 09:05:02 +02:00
1 parent 3f7d934fa5
commit 36b75a071b
20 files changed
+251 -115

No files matched your search

@@ -30,13 +30,11 @@ export const WidgetDynamicSelectInput = ({
const inputProps = form.getInputProps(`options.${property}`);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const { isPending, options: selectOptions } = options.useOptions(
debouncedSearch,
form.values.integrationIds,
form.values.options,
itemId,
boardId,
);
const {
error: optionsError,
isPending,
options: selectOptions,
} = options.useOptions(debouncedSearch, form.values.integrationIds, form.values.options, itemId, boardId);
const currentOption = inputProps.value as DynamicSelectOption | null;
const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void;
@@ -98,6 +96,7 @@ export const WidgetDynamicSelectInput = ({
description={options.withDescription ? tWidget("description") : undefined}
searchable
{...inputProps}
error={inputProps.error ?? optionsError}
value={currentOption === null ? null : currentOption.value}
onChange={(selectedValue: string | null) => {
if (selectedValue === null) {
@@ -102,23 +102,26 @@ const ArchiveTeamWarriorWidgetContent = ({
</Group>
<ScrollArea style={{ flex: 1 }}>
<Stack gap={4}>
{status.items.map((item) => (
<Card key={item.id} withBorder p="xs">
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} miw={0}>
<Text size="sm" fw={600} lineClamp={1}>
{item.name}
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
{item.project ?? projectName}
</Text>
</Stack>
<Badge size="xs" color={getStatusColor(item.status)}>
{t(`status.${item.status}`)}
</Badge>
</Group>
</Card>
))}
{status.items.map((item) => {
const itemStatusKey = getStatusKey(item.status);
return (
<Card key={item.id} withBorder p="xs">
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} miw={0}>
<Text size="sm" fw={600} lineClamp={1}>
{item.name}
</Text>
<Text size="xs" c="dimmed" lineClamp={1}>
{item.project ?? projectName}
</Text>
</Stack>
<Badge size="xs" color={getStatusColor(itemStatusKey)}>
{t(`status.${itemStatusKey}`)}
</Badge>
</Group>
</Card>
);
})}
</Stack>
</ScrollArea>
</>
@@ -9,6 +9,7 @@ import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { constructBoardPermissions } from "@homarr/auth/shared";
import { useOptionalBoard } from "@homarr/boards/context";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import classes from "./component.module.css";
@@ -36,7 +37,13 @@ export default function BeszelSystemStatsWidget({
const board = useOptionalBoard();
const { data: session } = useSession();
const hasChangeAccess = board ? constructBoardPermissions(board, session).hasChangeAccess : false;
const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation();
const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation({
onError: () =>
showErrorNotification({
title: t("error.selectionSaveTitle"),
message: t("error.selectionSaveMessage"),
}),
});
const {
data: systemsResult = [],
isPending: systemsPending,
@@ -405,7 +405,7 @@ export default function BeszelSystemTableWidget({
integrationIds: [activeIntegrationId],
systemId: activeSystem.id,
timePeriod: "1h",
includeDocker: true,
includeDocker: false,
},
"query",
),
@@ -472,9 +472,9 @@ export default function BeszelSystemTableWidget({
disk: options.showDisk,
diskIO: options.showDisk,
network: options.showNet,
dockerCpu: true,
dockerMemory: true,
dockerNetwork: true,
dockerCpu: false,
dockerMemory: false,
dockerNetwork: false,
}}
/>
</Stack>
+1
View File
@@ -134,6 +134,7 @@ export const setWidgetRuntimeQueries = (
queryKeys: readonly QueryKey[],
) => {
if (!widgetStateRef) return;
// This render-time ref write is idempotent and does not trigger a render.
const state = widgetStateRef.current ?? {};
state[runtimeQueriesStateKey] = queryKeys.flatMap((queryKey) => {
const query = normalizeWidgetQuery(queryKey);
@@ -69,8 +69,10 @@ export const SystemHealthMonitoring = ({
if (healthData.length === 0) return <WidgetEmptyState />;
const Container = withScrollArea ? ScrollArea : Box;
return (
<ScrollArea h={withScrollArea ? "100%" : undefined}>
<Container h={withScrollArea ? "100%" : undefined}>
<Stack mih="100%" gap="sm" className="health-monitoring">
{healthData.map(({ integrationId, integrationName, healthInfo }) => {
const filteredFileSystem = filterStorageVolumes(
@@ -221,7 +223,7 @@ export const SystemHealthMonitoring = ({
);
})}
</Stack>
</ScrollArea>
</Container>
);
};
+1
View File
@@ -59,6 +59,7 @@ interface DynamicSelectInput extends CommonInput<DynamicSelectOption | null> {
itemId?: string,
boardId?: string,
) => {
error?: string;
isPending: boolean;
options: DynamicSelectOption[];
};
+3 -3
View File
@@ -12,10 +12,10 @@ import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss";
const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => {
const { data: feedEntries = [], error } = clientApi.widget.rssFeed.getFeeds.useQuery(input);
if (error) throw error;
const { data: feedEntries, error } = clientApi.widget.rssFeed.getFeeds.useQuery(input);
if (error && feedEntries === undefined) throw error;
return feedEntries;
return feedEntries ?? [];
};
export default function RssFeed({ options, width, displayMode }: WidgetComponentProps<"rssFeed">) {
+7 -1
View File
@@ -1,4 +1,4 @@
import { IconRss } from "@tabler/icons-react";
import { IconRss, IconServerOff } from "@tabler/icons-react";
import { z } from "zod/v4";
import { createWidgetDefinition, widgetQueryInputMatches } from "../definition";
@@ -19,6 +19,12 @@ export const { definition, componentLoader } = createWidgetDefinition("rssFeed",
maximumAmountPosts: scope.options.maximumAmountPosts,
}),
refetchInterval: null,
errors: {
BAD_GATEWAY: {
icon: IconServerOff,
message: (t) => t("widget.rssFeed.error.allFeedsFailed"),
},
},
createOptions() {
return optionsBuilder.from((factory) => ({
feedUrls: factory.multiText({
+9 -1
View File
@@ -2,6 +2,7 @@ import { IconBusStop } from "@tabler/icons-react";
import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
@@ -13,10 +14,16 @@ export const { componentLoader, definition } = createWidgetDefinition("timetable
baseUrl: factory.text({
defaultValue: "https://search.ch",
validate: z.string().url(),
withDescription: true,
}),
station: factory.dynamicSelect({
useOptions(query, _integrationIds, options, itemId, boardId) {
const { data: stations, isPending } = clientApi.widget.timetable.searchStations.useQuery({
const t = useScopedI18n("widget.timetable");
const {
data: stations,
error,
isPending,
} = clientApi.widget.timetable.searchStations.useQuery({
baseUrl: typeof options.baseUrl === "string" ? options.baseUrl : "https://search.ch",
itemId,
boardId,
@@ -24,6 +31,7 @@ export const { componentLoader, definition } = createWidgetDefinition("timetable
});
return {
error: error ? t("error.stationSearch") : undefined,
isPending,
options:
stations?.map((station) => ({
+1 -1
View File
@@ -42,7 +42,7 @@ export default function UmamiWidget({
"query",
),
getQueryKey(clientApi.widget.umami.getActiveVisitors, { integrationId, websiteId: options.websiteId }, "query"),
...(options.viewMode === "events"
...(options.viewMode === "events" && options.eventNames.length > 0
? [
getQueryKey(
clientApi.widget.umami.getMultiEventTimeSeries,