Revert "feat: complete automatic mobile board UX"
This reverts commit 7959c20d88.
This commit is contained in:
1 parent
dba9a250ff
commit
b612fb9a89
85 files changed
+565
-2867
No files matched your search
@@ -7,16 +7,13 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
|
||||
import { WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./bookmark.module.css";
|
||||
|
||||
export default function BookmarksWidget({ options, itemId, displayMode }: WidgetComponentProps<"bookmarks">) {
|
||||
export default function BookmarksWidget({ options, itemId }: WidgetComponentProps<"bookmarks">) {
|
||||
const board = useRequiredBoard();
|
||||
const t = useI18n();
|
||||
const { data = [] } = clientApi.app.byIds.useQuery(options.items, {
|
||||
select(data) {
|
||||
return data.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||
@@ -44,21 +41,6 @@ export default function BookmarksWidget({ options, itemId, displayMode }: Widget
|
||||
[data],
|
||||
);
|
||||
|
||||
if (displayMode === "mobileSummary") {
|
||||
return (
|
||||
<WidgetMobileSummary
|
||||
value={data.length}
|
||||
label={options.title || t("widget.bookmarks.name")}
|
||||
description={
|
||||
data
|
||||
.map((app) => app.name)
|
||||
.slice(0, 2)
|
||||
.join(" · ") || t("board.mobile.details")
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack h="100%" gap="sm" p="sm">
|
||||
{options.title.length > 0 && (
|
||||
|
||||
@@ -10,13 +10,8 @@ import { BookmarkAddButton } from "./add-button";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("bookmarks", {
|
||||
icon: IconBookmark,
|
||||
mobile: {
|
||||
width: 2,
|
||||
height: 1,
|
||||
supportsCompactSummary: true,
|
||||
supportsDetailView: true,
|
||||
},
|
||||
queryKey: [["app", "byIds"]],
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["app"]],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
title: factory.text(),
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useSettings } from "@homarr/settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../common/query-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { CalendarDay } from "./calender-day";
|
||||
import classes from "./component.module.css";
|
||||
@@ -45,8 +44,8 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options, d
|
||||
const { data, error, isPending } = clientApi.widget.calendar.findAllEvents.useQuery(input);
|
||||
const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]);
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, data !== undefined);
|
||||
if (displayMode === "mobileSummary" && isPending) return <WidgetMobileLoading />;
|
||||
if (displayMode === "mobileSummary" && error && !data) throw error;
|
||||
|
||||
return (
|
||||
<CalendarBase
|
||||
@@ -56,11 +55,7 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options, d
|
||||
setMonth={setMonth}
|
||||
options={options}
|
||||
displayMode={displayMode}
|
||||
isStale={hasWidgetDataWarning({
|
||||
error,
|
||||
expectedIntegrationCount: integrationIds.length,
|
||||
receivedIntegrationCount: data?.length,
|
||||
})}
|
||||
isStale={Boolean(error) || (data?.length ?? 0) < integrationIds.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -130,7 +125,7 @@ const CalendarBase = ({
|
||||
);
|
||||
}
|
||||
|
||||
const calendar = (
|
||||
return (
|
||||
<Calendar
|
||||
defaultDate={new Date()}
|
||||
onPreviousMonth={(month) => setMonth(new Date(month))}
|
||||
@@ -210,7 +205,6 @@ const CalendarBase = ({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <WidgetDataState hasWarning={isStale}>{calendar}</WidgetDataState>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError } from "./query-state";
|
||||
|
||||
describe("widget query state", () => {
|
||||
test.each([
|
||||
{ error: new Error("refresh failed") },
|
||||
{ failedIntegrationCount: 1 },
|
||||
{ staleIntegrationCount: 1 },
|
||||
{ expectedIntegrationCount: 2, receivedIntegrationCount: 1 },
|
||||
])("warns when data provenance is incomplete or stale", (provenance) => {
|
||||
expect(hasWidgetDataWarning(provenance)).toBe(true);
|
||||
});
|
||||
|
||||
test("does not warn for complete current data", () => {
|
||||
expect(
|
||||
hasWidgetDataWarning({
|
||||
failedIntegrationCount: 0,
|
||||
staleIntegrationCount: 0,
|
||||
expectedIntegrationCount: 2,
|
||||
receivedIntegrationCount: 2,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("throws an initial query error", () => {
|
||||
const error = new Error("initial request failed");
|
||||
expect(() => throwOnInitialQueryError(error, false)).toThrow(error);
|
||||
});
|
||||
|
||||
test("keeps cached data visible after a refresh error", () => {
|
||||
expect(() => throwOnInitialQueryError(new Error("refresh failed"), true)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Center, Tooltip } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface WidgetQueryProvenance {
|
||||
error?: unknown;
|
||||
failedIntegrationCount?: number;
|
||||
staleIntegrationCount?: number;
|
||||
expectedIntegrationCount?: number;
|
||||
receivedIntegrationCount?: number;
|
||||
}
|
||||
|
||||
export const hasWidgetDataWarning = ({
|
||||
error,
|
||||
failedIntegrationCount = 0,
|
||||
staleIntegrationCount = 0,
|
||||
expectedIntegrationCount,
|
||||
receivedIntegrationCount,
|
||||
}: WidgetQueryProvenance) =>
|
||||
Boolean(error) ||
|
||||
failedIntegrationCount > 0 ||
|
||||
staleIntegrationCount > 0 ||
|
||||
(expectedIntegrationCount !== undefined &&
|
||||
receivedIntegrationCount !== undefined &&
|
||||
receivedIntegrationCount < expectedIntegrationCount);
|
||||
|
||||
export const throwOnInitialQueryError = (error: unknown, hasData: boolean) => {
|
||||
if (error && !hasData) throw error;
|
||||
};
|
||||
|
||||
interface WidgetDataStateProps {
|
||||
children: ReactNode;
|
||||
hasWarning: boolean;
|
||||
}
|
||||
|
||||
export function WidgetDataState({ children, hasWarning }: WidgetDataStateProps) {
|
||||
const t = useI18n();
|
||||
const label = t("board.mobile.dataWarning");
|
||||
|
||||
return (
|
||||
<Box pos="relative" h="100%" w="100%">
|
||||
{hasWarning && (
|
||||
<Tooltip label={label} position="left" withArrow>
|
||||
<Center
|
||||
component="output"
|
||||
aria-label={label}
|
||||
pos="absolute"
|
||||
bottom={4}
|
||||
w={28}
|
||||
h={28}
|
||||
style={{
|
||||
insetInlineStart: 4,
|
||||
zIndex: 4,
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
background: "light-dark(var(--mantine-color-yellow-1), var(--mantine-color-dark-6))",
|
||||
}}
|
||||
>
|
||||
<IconAlertTriangle
|
||||
size={16}
|
||||
color="light-dark(var(--mantine-color-yellow-9), var(--mantine-color-yellow-3))"
|
||||
aria-hidden
|
||||
/>
|
||||
</Center>
|
||||
</Tooltip>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("customApi", {
|
||||
icon: IconApi,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "customApi"]],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ export const widgetKind = "dnsHoleControls";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
|
||||
icon: IconDeviceGamepad,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "dnsHole"]],
|
||||
refetchInterval: 5,
|
||||
createOptions() {
|
||||
|
||||
@@ -9,7 +9,6 @@ export const widgetKind = "dnsHoleSummary";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
|
||||
icon: IconAd,
|
||||
mobile: { unmountWhenOffscreen: true },
|
||||
queryKey: [["widget", "dnsHole"]],
|
||||
refetchInterval: 5,
|
||||
createOptions() {
|
||||
|
||||
@@ -51,8 +51,6 @@ import type { ExtendedClientStatus, ExtendedDownloadClientItem } from "@homarr/i
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError } from "../common/query-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -280,57 +278,7 @@ function SpeedCell({
|
||||
);
|
||||
}
|
||||
|
||||
export default function DownloadsWidget(props: WidgetComponentProps<"downloads">) {
|
||||
if (props.displayMode === "mobileSummary") {
|
||||
return <DownloadsMobileSummary integrationIds={props.integrationIds} options={props.options} />;
|
||||
}
|
||||
|
||||
return <DownloadClientsWidget {...props} />;
|
||||
}
|
||||
|
||||
function DownloadsMobileSummary({
|
||||
integrationIds,
|
||||
options,
|
||||
}: Pick<WidgetComponentProps<"downloads">, "integrationIds" | "options">) {
|
||||
const t = useScopedI18n("widget.downloads");
|
||||
const { data, error, isPending } = clientApi.widget.downloads.getJobsAndStatuses.useQuery({
|
||||
integrationIds,
|
||||
limitPerIntegration: options.limitPerIntegration,
|
||||
});
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, data !== undefined);
|
||||
|
||||
const currentItems = data ?? [];
|
||||
const selectedClients = currentItems.filter(({ integration }) => integrationIds.includes(integration.id));
|
||||
const visibleItemCount = selectedClients.reduce(
|
||||
(total, { data: clientData }) =>
|
||||
total +
|
||||
clientData.items
|
||||
.filter(({ category }) => matchesCategoryFilter(category, options.categoryFilter, options.filterIsWhitelist))
|
||||
.filter((item) => showCompletedItem(item, options)).length,
|
||||
0,
|
||||
);
|
||||
const totalDownloadSpeed = selectedClients.reduce(
|
||||
(total, { data: clientData }) => total + (clientData.status.rates.down ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<WidgetMobileSummary
|
||||
value={visibleItemCount}
|
||||
label={t("name")}
|
||||
description={`${t("items.downSpeed.columnTitle")}: ${formatByteRate(totalDownloadSpeed)}`}
|
||||
isStale={hasWidgetDataWarning({
|
||||
error,
|
||||
expectedIntegrationCount: integrationIds.length,
|
||||
receivedIntegrationCount: currentItems.length,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadClientsWidget({
|
||||
export default function DownloadClientsWidget({
|
||||
isEditMode,
|
||||
integrationIds,
|
||||
options,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("firewall", {
|
||||
icon: IconWall,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "firewall"]],
|
||||
refetchInterval: 5,
|
||||
createOptions() {
|
||||
|
||||
@@ -16,7 +16,6 @@ import type { TablerIcon } from "@homarr/ui";
|
||||
import { OverflowBadge } from "@homarr/ui";
|
||||
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../common/query-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
export default function MediaReleasesWidget({
|
||||
@@ -24,33 +23,29 @@ export default function MediaReleasesWidget({
|
||||
integrationIds,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"mediaReleases">) {
|
||||
const { data, error, isPending } = clientApi.widget.mediaRelease.getMediaReleasesWithProvenance.useQuery({
|
||||
const { data, error, isPending } = clientApi.widget.mediaRelease.getMediaReleases.useQuery({
|
||||
integrationIds,
|
||||
});
|
||||
const t = useI18n("widget.mediaReleases");
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, data !== undefined);
|
||||
|
||||
const releases = data?.items ?? [];
|
||||
const hasWarning = hasWidgetDataWarning({
|
||||
error,
|
||||
failedIntegrationCount: data?.failedIntegrationCount,
|
||||
staleIntegrationCount: data?.staleIntegrationCount,
|
||||
});
|
||||
|
||||
if (displayMode === "mobileSummary") {
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
if (error && !data) throw error;
|
||||
|
||||
const releases = data ?? [];
|
||||
return (
|
||||
<WidgetMobileSummary
|
||||
value={releases.length}
|
||||
label={t("name")}
|
||||
description={releases[0]?.title}
|
||||
isStale={hasWarning}
|
||||
isStale={Boolean(error)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const content = (
|
||||
const releases = data ?? [];
|
||||
|
||||
return (
|
||||
<Stack p="xs" gap="sm">
|
||||
{releases.map((item, index) => (
|
||||
<Fragment key={item.id}>
|
||||
@@ -60,11 +55,10 @@ export default function MediaReleasesWidget({
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
return <WidgetDataState hasWarning={hasWarning}>{content}</WidgetDataState>;
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
item: RouterOutputs["widget"]["mediaRelease"]["getMediaReleasesWithProvenance"]["items"][number];
|
||||
item: RouterOutputs["widget"]["mediaRelease"]["getMediaReleases"][number];
|
||||
options: WidgetComponentProps<"mediaReleases">["options"];
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../../common/query-state";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||
import classes from "../search-button.module.css";
|
||||
@@ -40,7 +39,11 @@ export default function MediaServerWidget({
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"mediaRequests-requestList">) {
|
||||
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||
const { data, error, isPending } = clientApi.widget.mediaRequests.getLatestRequestsWithProvenance.useQuery({
|
||||
const {
|
||||
data: mediaRequests,
|
||||
error,
|
||||
isPending,
|
||||
} = clientApi.widget.mediaRequests.getLatestRequests.useQuery({
|
||||
integrationIds,
|
||||
statuses:
|
||||
options.statusFilter.length > 0
|
||||
@@ -49,38 +52,23 @@ export default function MediaServerWidget({
|
||||
recentDays: options.recentDays,
|
||||
});
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, data !== undefined);
|
||||
if (!data) return <WidgetEmptyState />;
|
||||
|
||||
const hasWarning = hasWidgetDataWarning({
|
||||
error,
|
||||
failedIntegrationCount: data.failedIntegrationCount,
|
||||
staleIntegrationCount: data.staleIntegrationCount,
|
||||
});
|
||||
|
||||
if (data.items.length === 0 && !hasWarning) throw new NoIntegrationDataError();
|
||||
if (displayMode === "mobileSummary" && isPending) return <WidgetMobileLoading />;
|
||||
if (displayMode === "mobileSummary" && error && !mediaRequests) throw error;
|
||||
if (!mediaRequests) return <WidgetEmptyState />;
|
||||
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
|
||||
|
||||
if (displayMode === "mobileSummary") {
|
||||
return (
|
||||
<WidgetMobileSummary
|
||||
value={data.items.length}
|
||||
value={mediaRequests.length}
|
||||
label={t("name")}
|
||||
description={data.items[0]?.name}
|
||||
isStale={hasWarning}
|
||||
description={mediaRequests[0]?.name}
|
||||
isStale={Boolean(error)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.items.length === 0) {
|
||||
return (
|
||||
<WidgetDataState hasWarning={hasWarning}>
|
||||
<WidgetEmptyState />
|
||||
</WidgetDataState>
|
||||
);
|
||||
}
|
||||
|
||||
const content = (
|
||||
return (
|
||||
<Box className={classes.searchRoot}>
|
||||
{!isEditMode && <MediaRequestSearchButton integrationIds={integrationIds} />}
|
||||
<ScrollArea
|
||||
@@ -89,7 +77,7 @@ export default function MediaServerWidget({
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
|
||||
{data.items.map((mediaRequest) => (
|
||||
{mediaRequests.map((mediaRequest) => (
|
||||
<MediaRequestCard
|
||||
key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
|
||||
request={mediaRequest}
|
||||
@@ -101,7 +89,6 @@ export default function MediaServerWidget({
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
);
|
||||
return <WidgetDataState hasWarning={hasWarning}>{content}</WidgetDataState>;
|
||||
}
|
||||
|
||||
const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
|
||||
@@ -123,7 +110,7 @@ const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[]
|
||||
};
|
||||
|
||||
interface MediaRequestCardProps {
|
||||
request: RouterOutputs["widget"]["mediaRequests"]["getLatestRequestsWithProvenance"]["items"][number];
|
||||
request: RouterOutputs["widget"]["mediaRequests"]["getLatestRequests"][number];
|
||||
isTiny: boolean;
|
||||
options: WidgetComponentProps<"mediaRequests-requestList">["options"];
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../../common/query-state";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||
import classes from "./component.module.css";
|
||||
@@ -36,30 +34,16 @@ export default function MediaServerWidget({
|
||||
integrationIds,
|
||||
isEditMode,
|
||||
width,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"mediaRequests-requestStats">) {
|
||||
const t = useScopedI18n("widget.mediaRequests-requestStats");
|
||||
const {
|
||||
data: requestStats,
|
||||
error,
|
||||
isPending,
|
||||
} = clientApi.widget.mediaRequests.getStatsWithProvenance.useQuery({
|
||||
const { data: requestStats } = clientApi.widget.mediaRequests.getStats.useQuery({
|
||||
integrationIds,
|
||||
});
|
||||
|
||||
const board = useRequiredBoard();
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, requestStats !== undefined);
|
||||
if (!requestStats) return <WidgetEmptyState />;
|
||||
const hasWarning = hasWidgetDataWarning({
|
||||
error,
|
||||
failedIntegrationCount: requestStats.failedIntegrationCount,
|
||||
staleIntegrationCount: requestStats.staleIntegrationCount,
|
||||
});
|
||||
if (requestStats.users.length === 0 && requestStats.stats.length === 0 && !hasWarning) {
|
||||
throw new NoIntegrationDataError();
|
||||
}
|
||||
if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError();
|
||||
|
||||
const data = [
|
||||
{
|
||||
@@ -104,23 +88,9 @@ export default function MediaServerWidget({
|
||||
},
|
||||
] satisfies { name: keyof RequestStats; icon: Icon; number: number }[];
|
||||
|
||||
if (displayMode === "mobileSummary") {
|
||||
const total = data.find((stat) => stat.name === "total")?.number ?? 0;
|
||||
const pending = data.find((stat) => stat.name === "pending")?.number ?? 0;
|
||||
|
||||
return (
|
||||
<WidgetMobileSummary
|
||||
value={total}
|
||||
label={t("titles.stats.total")}
|
||||
description={`${pending} ${t("titles.stats.pending")}`}
|
||||
isStale={hasWarning}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isTiny = width < 256;
|
||||
|
||||
const content = (
|
||||
return (
|
||||
<Box className={searchClasses.searchRoot}>
|
||||
{!isEditMode && <MediaRequestSearchButton integrationIds={integrationIds} />}
|
||||
<Stack
|
||||
@@ -203,7 +173,6 @@ export default function MediaServerWidget({
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
return <WidgetDataState hasWarning={hasWarning}>{content}</WidgetDataState>;
|
||||
}
|
||||
|
||||
const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
|
||||
|
||||
@@ -7,12 +7,7 @@ import { createWidgetDefinition } from "../../definition";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", {
|
||||
icon: IconChartBar,
|
||||
mobile: {
|
||||
width: 2,
|
||||
height: 1,
|
||||
supportsCompactSummary: true,
|
||||
supportsDetailView: true,
|
||||
},
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "mediaRequests"]],
|
||||
createOptions() {
|
||||
return {};
|
||||
|
||||
@@ -25,7 +25,6 @@ import type { TablerIcon } from "@homarr/ui";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../common/query-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
export default function MediaServerWidget({ displayMode, ...props }: WidgetComponentProps<"mediaServer">) {
|
||||
@@ -44,9 +43,10 @@ const MediaServerMobileSummary = ({
|
||||
});
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, data !== undefined);
|
||||
if (error && !data) throw error;
|
||||
|
||||
const currentStreams = data ?? [];
|
||||
const hasPartialFailure = currentStreams.length < integrationIds.length;
|
||||
const playingSessions = currentStreams
|
||||
.flatMap((stream) => stream.sessions)
|
||||
.filter((session) => session.currentlyPlaying !== null);
|
||||
@@ -56,11 +56,7 @@ const MediaServerMobileSummary = ({
|
||||
value={playingSessions.length}
|
||||
label={t("items.currentlyPlaying")}
|
||||
description={playingSessions[0]?.currentlyPlaying?.name}
|
||||
isStale={hasWidgetDataWarning({
|
||||
error,
|
||||
expectedIntegrationCount: integrationIds.length,
|
||||
receivedIntegrationCount: currentStreams.length,
|
||||
})}
|
||||
isStale={Boolean(error) || hasPartialFailure}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -69,11 +65,10 @@ const MediaServerTable = ({
|
||||
options,
|
||||
integrationIds,
|
||||
}: Pick<WidgetComponentProps<"mediaServer">, "options" | "integrationIds">) => {
|
||||
const { data: queryData, error } = clientApi.widget.mediaServer.getCurrentStreams.useQuery({
|
||||
const { data: currentStreams = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery({
|
||||
integrationIds,
|
||||
showOnlyPlaying: options.showOnlyPlaying,
|
||||
});
|
||||
const currentStreams = queryData ?? [];
|
||||
|
||||
const t = useScopedI18n("widget.mediaServer");
|
||||
const columns = useMemo<MRT_ColumnDef<StreamSession>[]>(
|
||||
@@ -297,14 +292,8 @@ const MediaServerTable = ({
|
||||
integrationName: session?.integrationName,
|
||||
};
|
||||
});
|
||||
throwOnInitialQueryError(error, queryData !== undefined);
|
||||
const hasWarning = hasWidgetDataWarning({
|
||||
error,
|
||||
expectedIntegrationCount: integrationIds.length,
|
||||
receivedIntegrationCount: currentStreams.length,
|
||||
});
|
||||
|
||||
const content = (
|
||||
return (
|
||||
<Stack gap={0} h="100%" display="flex">
|
||||
<MantineReactTable table={table} />
|
||||
<Group
|
||||
@@ -328,7 +317,6 @@ const MediaServerTable = ({
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
return <WidgetDataState hasWarning={hasWarning}>{content}</WidgetDataState>;
|
||||
};
|
||||
|
||||
const ItemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTimeAgo } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../common/query-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
export default function NotificationsWidget({
|
||||
@@ -39,20 +38,17 @@ export default function NotificationsWidget({
|
||||
[notificationIntegrations],
|
||||
);
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, notificationIntegrations !== undefined);
|
||||
const hasWarning = hasWidgetDataWarning({
|
||||
error,
|
||||
expectedIntegrationCount: integrationIds.length,
|
||||
receivedIntegrationCount: notificationIntegrations?.length,
|
||||
});
|
||||
|
||||
if (displayMode === "mobileSummary") {
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
if (error && !notificationIntegrations) throw error;
|
||||
|
||||
const notifications = notificationIntegrations ?? [];
|
||||
const latestNotification = sortedNotifications[0];
|
||||
const isStale = Boolean(error) || notifications.length < integrationIds.length;
|
||||
const summary = latestNotification ? (
|
||||
<NotificationMobileSummary notification={latestNotification} isStale={hasWarning} />
|
||||
<NotificationMobileSummary notification={latestNotification} isStale={isStale} />
|
||||
) : (
|
||||
<WidgetMobileSummary value={0} label={t("name")} description={t("noItems")} isStale={hasWarning} />
|
||||
<WidgetMobileSummary value={0} label={t("name")} description={t("noItems")} isStale={isStale} />
|
||||
);
|
||||
|
||||
if (!latestNotification?.href) return summary;
|
||||
@@ -72,7 +68,7 @@ export default function NotificationsWidget({
|
||||
);
|
||||
}
|
||||
|
||||
const content = (
|
||||
return (
|
||||
<ScrollArea className="scroll-area-w100" w="100%" p="sm">
|
||||
<Stack w={"100%"} gap="sm">
|
||||
{sortedNotifications.length > 0 ? (
|
||||
@@ -121,7 +117,6 @@ export default function NotificationsWidget({
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
);
|
||||
return <WidgetDataState hasWarning={hasWarning}>{content}</WidgetDataState>;
|
||||
}
|
||||
|
||||
const NotificationMobileSummary = ({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("tracearr", {
|
||||
icon: IconActivityHeartbeat,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "tracearr"]],
|
||||
refetchInterval: 5,
|
||||
createOptions() {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("umami", {
|
||||
icon: IconChartBar,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "umami"]],
|
||||
refetchInterval: null,
|
||||
supportedIntegrations: ["umami"],
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
|
||||
import { hasWidgetDataWarning, throwOnInitialQueryError, WidgetDataState } from "../common/query-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { NoIntegrationDataError } from "../errors/no-data-integration";
|
||||
import classes from "./component.module.css";
|
||||
@@ -118,15 +117,11 @@ function UptimeKumaContent({ integrationIds, options, width, displayMode }: Upti
|
||||
} = clientApi.widget.uptimeKuma.getDashboard.useQuery({
|
||||
integrationIds,
|
||||
});
|
||||
const hasPartialFailure = (dashboardData?.length ?? 0) < integrationIds.length;
|
||||
|
||||
if (isPending) return <WidgetMobileLoading />;
|
||||
throwOnInitialQueryError(error, dashboardData !== undefined);
|
||||
if (displayMode === "mobileSummary" && isPending) return <WidgetMobileLoading />;
|
||||
if (displayMode === "mobileSummary" && error && !dashboardData) throw error;
|
||||
if (!dashboardData) return <WidgetEmptyState />;
|
||||
const hasWarning = hasWidgetDataWarning({
|
||||
error,
|
||||
expectedIntegrationCount: integrationIds.length,
|
||||
receivedIntegrationCount: dashboardData.length,
|
||||
});
|
||||
|
||||
const combined = dashboardData.reduce<UptimeKumaDashboardData>(
|
||||
(acc, item) => ({
|
||||
@@ -168,13 +163,13 @@ function UptimeKumaContent({ integrationIds, options, width, displayMode }: Upti
|
||||
value={formatNumber(combined[primaryStat], 0)}
|
||||
label={t(`stats.${primaryStat}`)}
|
||||
description={supportingDescription}
|
||||
isStale={hasWarning}
|
||||
isStale={Boolean(error) || hasPartialFailure}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasContent) {
|
||||
return <WidgetMobileSummary value="—" label={t("name")} isStale={hasWarning} />;
|
||||
return <WidgetMobileSummary value="—" label={t("name")} isStale={Boolean(error) || hasPartialFailure} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -182,7 +177,7 @@ function UptimeKumaContent({ integrationIds, options, width, displayMode }: Upti
|
||||
value={`${formatNumber(uptimeValue, 1)}%`}
|
||||
label={t("averageUptime")}
|
||||
description={supportingDescription}
|
||||
isStale={hasWarning}
|
||||
isStale={Boolean(error) || hasPartialFailure}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -191,7 +186,7 @@ function UptimeKumaContent({ integrationIds, options, width, displayMode }: Upti
|
||||
heroLayoutBySecondaryStats[String(visibleStatKeys.length > 0) as keyof typeof heroLayoutBySecondaryStats];
|
||||
const heroRingClass = heroVariantByRing[String(options.showUptimeRing) as keyof typeof heroVariantByRing];
|
||||
|
||||
const content = (
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{showHero && (
|
||||
<div className={`${classes.hero} ${heroLayoutClass} ${heroRingClass}`}>
|
||||
@@ -246,7 +241,6 @@ function UptimeKumaContent({ integrationIds, options, width, displayMode }: Upti
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <WidgetDataState hasWarning={hasWarning}>{content}</WidgetDataState>;
|
||||
}
|
||||
|
||||
function clampPercent(value: number): number {
|
||||
|
||||
Reference in new issue
Block a user