fix: complete widget review cleanup
This commit is contained in:
1 parent
56346b2aa8
commit
f003d9ddc9
17 files changed
+279
-145
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>
|
||||
</>
|
||||
|
||||
@@ -8,6 +8,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";
|
||||
@@ -35,7 +36,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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ interface DynamicSelectInput extends CommonInput<DynamicSelectOption | null> {
|
||||
itemId?: string,
|
||||
boardId?: string,
|
||||
) => {
|
||||
error?: string;
|
||||
isPending: boolean;
|
||||
options: DynamicSelectOption[];
|
||||
};
|
||||
|
||||
@@ -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">) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { setWidgetRuntimeQueries } from "../definition";
|
||||
import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import { UmamiContent } from "./umami-content";
|
||||
|
||||
export default function UmamiWidget({
|
||||
@@ -14,13 +16,8 @@ export default function UmamiWidget({
|
||||
width,
|
||||
height,
|
||||
displayMode = "compact",
|
||||
widgetStateRef,
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"umami">) {
|
||||
if (!options.websiteId || options.websiteId.trim() === "") {
|
||||
setWidgetRuntimeQueries(widgetStateRef, []);
|
||||
return <NoWebsiteConfigured />;
|
||||
}
|
||||
|
||||
const integrationId = integrationIds[0] ?? "";
|
||||
const commonTopInput = {
|
||||
integrationId,
|
||||
@@ -28,40 +25,51 @@ export default function UmamiWidget({
|
||||
timeFrame: options.timeFrame,
|
||||
limit: options.topCount,
|
||||
};
|
||||
setWidgetRuntimeQueries(widgetStateRef, [
|
||||
{
|
||||
path: ["widget", "umami", "getVisitorStats"],
|
||||
input: {
|
||||
integrationIds,
|
||||
websiteId: options.websiteId,
|
||||
timeFrame: options.timeFrame,
|
||||
eventName: options.eventName || undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ["widget", "umami", "getActiveVisitors"],
|
||||
input: { integrationId, websiteId: options.websiteId },
|
||||
},
|
||||
...(options.viewMode === "events"
|
||||
const hasWebsite = Boolean(options.websiteId?.trim());
|
||||
useWidgetRuntimeQueries(
|
||||
widgetRuntimeRef,
|
||||
hasWebsite
|
||||
? [
|
||||
{
|
||||
path: ["widget", "umami", "getMultiEventTimeSeries"],
|
||||
input: {
|
||||
integrationId,
|
||||
getQueryKey(
|
||||
clientApi.widget.umami.getVisitorStats,
|
||||
{
|
||||
integrationIds,
|
||||
websiteId: options.websiteId,
|
||||
timeFrame: options.timeFrame,
|
||||
eventNames: [...options.eventNames].toSorted(),
|
||||
eventName: options.eventName || undefined,
|
||||
},
|
||||
},
|
||||
"query",
|
||||
),
|
||||
getQueryKey(
|
||||
clientApi.widget.umami.getActiveVisitors,
|
||||
{ integrationId, websiteId: options.websiteId },
|
||||
"query",
|
||||
),
|
||||
...(options.viewMode === "events" && options.eventNames.length > 0
|
||||
? [
|
||||
getQueryKey(
|
||||
clientApi.widget.umami.getMultiEventTimeSeries,
|
||||
{
|
||||
integrationId,
|
||||
websiteId: options.websiteId,
|
||||
timeFrame: options.timeFrame,
|
||||
eventNames: [...options.eventNames].toSorted(),
|
||||
},
|
||||
"query",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(displayMode === "advanced" || options.viewMode === "topPages"
|
||||
? [getQueryKey(clientApi.widget.umami.getTopPages, commonTopInput, "query")]
|
||||
: []),
|
||||
...(displayMode === "advanced" || options.viewMode === "topReferrers"
|
||||
? [getQueryKey(clientApi.widget.umami.getTopReferrers, commonTopInput, "query")]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(displayMode === "advanced" || options.viewMode === "topPages"
|
||||
? [{ path: ["widget", "umami", "getTopPages"], input: commonTopInput }]
|
||||
: []),
|
||||
...(displayMode === "advanced" || options.viewMode === "topReferrers"
|
||||
? [{ path: ["widget", "umami", "getTopReferrers"], input: commonTopInput }]
|
||||
: []),
|
||||
]);
|
||||
: [],
|
||||
);
|
||||
|
||||
if (!hasWebsite) return <NoWebsiteConfigured />;
|
||||
|
||||
return (
|
||||
<UmamiContent
|
||||
|
||||
Reference in new issue
Block a user