Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e56112cfd7 | ||
|
|
0d9eeb5752 | ||
|
|
bf0878c68b | ||
|
|
eb6d337dfb | ||
|
|
978b3c7f33 | ||
|
|
07547320f1 | ||
|
|
b6fc27e44d | ||
|
|
9eafd4f7a7 | ||
|
|
71ffa5d1be |
No files matched your search
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: homarr-local-harness
|
||||
description: Build and inspect a writable Docker demo harness from the current Homarr checkout when validating a local branch in a browser.
|
||||
---
|
||||
|
||||
# Homarr Local Harness
|
||||
|
||||
Use this skill when a Homarr change needs a real production image and browser validation. Work from the repository root and keep the harness name Docker-safe, such as `feature-board`.
|
||||
|
||||
## Set up the harness
|
||||
|
||||
Run the TypeScript operations CLI; this workflow does not use the interactive developer TUI:
|
||||
|
||||
```sh
|
||||
pnpm cli harness setup feature-board
|
||||
```
|
||||
|
||||
`setup` builds the current checkout as `homarr:feature-board`, replaces the named harness container, and starts it with a persistent data volume. It enables demo mode, mock integrations, and writable demo mutations by default:
|
||||
|
||||
- `DEMO_MODE=true`
|
||||
- `DEMO_READ_ONLY=false`
|
||||
- `UNSAFE_ENABLE_MOCK_INTEGRATION=true`
|
||||
|
||||
Pass repeatable environment overrides when the feature needs them:
|
||||
|
||||
```sh
|
||||
pnpm cli harness setup feature-board \
|
||||
--env WORKSHOP_WEB_URL=https://example.test \
|
||||
--env FEATURE_FLAG=true
|
||||
```
|
||||
|
||||
Use `pnpm cli harness build <name>` when only the image is needed, and `pnpm cli harness run <name>` to start an already-built image. `--port <number>` pins a host port; otherwise Docker assigns one.
|
||||
|
||||
Always use the URL printed by the command. If the output is no longer available, query it directly:
|
||||
|
||||
```sh
|
||||
pnpm cli harness port feature-board
|
||||
```
|
||||
|
||||
Do not assume port `7575`; the container listens on `7575`, but the host port is dynamic by default.
|
||||
|
||||
## Browser validation
|
||||
|
||||
Open the printed URL with the environment's `browser-harness` tool. In this workspace, the `agent-browser` CLI is the supported browser automation fallback. Load its current workflow before invoking it:
|
||||
|
||||
```sh
|
||||
agent-browser skills get core
|
||||
```
|
||||
|
||||
Navigate to the exact harness URL, wait for the app to become usable, exercise the changed flow, and capture screenshots or other browser evidence. Keep browser state in a named session when the workflow needs login persistence. Report the exact URL, viewport/session used, and screenshot paths; a Docker process or HTTP health response alone is not browser acceptance.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Stop the named container when finished:
|
||||
|
||||
```sh
|
||||
pnpm cli harness stop feature-board
|
||||
```
|
||||
|
||||
Stopping keeps the data volume so the same harness can be restarted. A later `setup` rebuilds the image and replaces only the container. Preserve unrelated containers, images, volumes, and working-tree changes.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "Homarr Local Harness"
|
||||
short_description: "Build and browse a writable local Homarr branch harness"
|
||||
default_prompt: "Use $homarr-local-harness to set up a writable Homarr harness for the current branch and validate it in the browser."
|
||||
@@ -111,9 +111,6 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
|
||||
|
||||
const nextBreakpoint = getNextCustomBreakpoint(form.values.layouts);
|
||||
const baseLayout = form.values.layouts.find((layout) => layout.role === "base");
|
||||
const sortedLayouts = form.values.layouts
|
||||
.map((layout, index) => ({ layout, index }))
|
||||
.toSorted((entryA, entryB) => entryA.layout.breakpoint - entryB.layout.breakpoint);
|
||||
|
||||
return (
|
||||
<SectionCard title={tBoard("setting.section.layout.title")}>
|
||||
@@ -129,25 +126,25 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
|
||||
disabled={nextBreakpoint === null || !baseLayout}
|
||||
onClick={() => {
|
||||
if (nextBreakpoint === null || !baseLayout) return;
|
||||
form.setFieldValue("layouts", [
|
||||
...form.values.layouts,
|
||||
{
|
||||
id: createId(),
|
||||
name: tBoard("setting.section.layout.custom.defaultName"),
|
||||
columnCount: baseLayout.columnCount,
|
||||
leftGutterColumnCount: baseLayout.leftGutterColumnCount,
|
||||
rightGutterColumnCount: baseLayout.rightGutterColumnCount,
|
||||
breakpoint: nextBreakpoint,
|
||||
role: "custom",
|
||||
},
|
||||
]);
|
||||
const newLayout: FormValues["layouts"][number] = {
|
||||
id: createId(),
|
||||
name: tBoard("setting.section.layout.custom.defaultName"),
|
||||
columnCount: baseLayout.columnCount,
|
||||
leftGutterColumnCount: baseLayout.leftGutterColumnCount,
|
||||
rightGutterColumnCount: baseLayout.rightGutterColumnCount,
|
||||
breakpoint: nextBreakpoint,
|
||||
role: "custom",
|
||||
};
|
||||
let insertionIndex = form.values.layouts.findIndex((layout) => layout.breakpoint > nextBreakpoint);
|
||||
if (insertionIndex === -1) insertionIndex = form.values.layouts.length;
|
||||
form.insertListItem("layouts", newLayout, insertionIndex);
|
||||
}}
|
||||
>
|
||||
{tBoard("setting.section.layout.responsive.action.add")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{sortedLayouts.map(({ layout, index }) => {
|
||||
{form.values.layouts.map((layout, index) => {
|
||||
const persistedLayout = board.layouts.find((candidate) => candidate.id === layout.id);
|
||||
const sourceLayout = persistedLayout ?? board.layouts.find((candidate) => candidate.role === "base");
|
||||
const customBreakpoints = form.values.layouts
|
||||
|
||||
@@ -184,10 +184,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.widgetPanel[data-editing] .panelActions {
|
||||
padding-inline-end: 2.75rem;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
min-width: 0;
|
||||
min-height: 3.75rem;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.settingsButton {
|
||||
position: absolute;
|
||||
top: -34px;
|
||||
right: 14px;
|
||||
z-index: 201;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease;
|
||||
@@ -45,3 +49,11 @@
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settingsButton {
|
||||
right: 4px;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,6 @@ const BoardItemMenuInner = ({ item, definition, previewDimensions, resetErrorBou
|
||||
variant="default"
|
||||
size={24}
|
||||
radius="sm"
|
||||
pos="absolute"
|
||||
top={4}
|
||||
right={4}
|
||||
style={{ zIndex: 30 }}
|
||||
className={itemContentClasses.settingsButton}
|
||||
data-menu-open={isMenuOpen || undefined}
|
||||
data-board-widget-settings
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 28px;
|
||||
padding: 4px 8px;
|
||||
padding: 4px 40px 4px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background-color: var(--background-color);
|
||||
|
||||
@@ -1904,8 +1904,7 @@
|
||||
}
|
||||
},
|
||||
"widgetQuery": {
|
||||
"loading": "Loading widget data",
|
||||
"stale": "Could not refresh {widget}; showing saved data"
|
||||
"loading": "Loading widget data"
|
||||
},
|
||||
"total": "Total",
|
||||
"filesystemUsage": "{count, plural, one {Show usage for # filesystem} other {Show usage for # filesystems}}",
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MantineProviderProps } from "@mantine/core";
|
||||
|
||||
import { theme } from "./theme";
|
||||
|
||||
export { theme } from "./theme";
|
||||
export { scrollAreaDefaultProps, theme } from "./theme";
|
||||
export { modalSizeForm, modalSizeSelect } from "./theme/modal";
|
||||
export * from "./components";
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import { IntegrationAvatar } from "../components/integration-avatar";
|
||||
import { SelectableCard } from "../components/selectable-card";
|
||||
import { selectGridCols } from "../select-grid-layout";
|
||||
import { buildSortedIntegrations, categoryTranslationKeys, filterIntegrations } from "./integration-grid-shared";
|
||||
import { buildSortedIntegrations, filterIntegrations } from "./integration-grid-shared";
|
||||
import type { IntegrationGridItem } from "./integration-grid-shared";
|
||||
|
||||
export interface IntegrationSelectGridProps {
|
||||
@@ -134,8 +134,11 @@ const IntegrationCard = ({
|
||||
onClick={() => onSelect(integration.kind)}
|
||||
aria-label={integration.name}
|
||||
icon={<IntegrationAvatar kind={integration.kind} size="sm" />}
|
||||
title={integration.name}
|
||||
topRight={<IntegrationCategoryBadges categories={integration.categories} limit={1} />}
|
||||
title={
|
||||
<Text fw={700} size="md" style={{ whiteSpace: "nowrap" }}>
|
||||
{integration.name}
|
||||
</Text>
|
||||
}
|
||||
footerLeft={
|
||||
connectedCount > 0 ? (
|
||||
<Badge variant="light" color="teal" size="xs" radius="xs">
|
||||
@@ -232,24 +235,3 @@ const IntegrationTiedWidgets = ({ widgets, limit }: { widgets: WidgetKind[]; lim
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const IntegrationCategoryBadges = ({
|
||||
categories,
|
||||
limit,
|
||||
}: {
|
||||
categories: IntegrationGridItem["categories"];
|
||||
limit?: number;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const items = limit ? categories.slice(0, limit) : categories;
|
||||
|
||||
return (
|
||||
<Group gap={4} wrap="wrap">
|
||||
{items.map((category) => (
|
||||
<Badge key={category} variant="light" size="xs" color="gray" radius="xs">
|
||||
{categoryTranslationKeys[category] ? t(categoryTranslationKeys[category] as never) : category}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,13 @@ import { createTheme, rem } from "@mantine/core";
|
||||
|
||||
import { modalComponent } from "./theme/modal";
|
||||
|
||||
export const scrollAreaDefaultProps = {
|
||||
type: "hover",
|
||||
scrollbarSize: 2,
|
||||
scrollHideDelay: 500,
|
||||
offsetScrollbars: true,
|
||||
} as const;
|
||||
|
||||
export const theme = createTheme({
|
||||
primaryColor: "red",
|
||||
autoContrast: true,
|
||||
@@ -96,6 +103,12 @@ export const theme = createTheme({
|
||||
style: { borderRadius: 5 },
|
||||
},
|
||||
},
|
||||
ScrollArea: {
|
||||
defaultProps: scrollAreaDefaultProps,
|
||||
},
|
||||
ScrollAreaAutosize: {
|
||||
defaultProps: scrollAreaDefaultProps,
|
||||
},
|
||||
LoadingOverlay: {
|
||||
defaultProps: {
|
||||
zIndex: 1000,
|
||||
|
||||
@@ -324,7 +324,7 @@ export function JsonPathTreePicker({
|
||||
</Combobox.Header>
|
||||
)}
|
||||
<Combobox.Options>
|
||||
<ScrollArea.Autosize mah={420} type="scroll">
|
||||
<ScrollArea.Autosize mah={420}>
|
||||
{!hasData && <Combobox.Empty>{noDataHint}</Combobox.Empty>}
|
||||
{hasData && visibleNodes.length === 0 && <Combobox.Empty>No matching paths</Combobox.Empty>}
|
||||
{visibleNodes.map((node) => (
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { formatLocalizedDate, formatLocalizedTime } from "../common/locale";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import type { AirQualityData, PollutantKey, PollenKey } from "./display";
|
||||
import {
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from "./display";
|
||||
|
||||
export default function AirQualityWidget({ options, width, height, displayMode }: WidgetComponentProps<"airQuality">) {
|
||||
const t = useI18n("widget.airQuality");
|
||||
const query = clientApi.widget.airQuality.atLocation.useQuery({
|
||||
latitude: options.location.latitude,
|
||||
longitude: options.location.longitude,
|
||||
@@ -45,11 +44,6 @@ export default function AirQualityWidget({ options, width, height, displayMode }
|
||||
) : (
|
||||
<CompactAirQuality airQuality={airQuality} height={height} options={options} width={width} />
|
||||
)}
|
||||
{query.error && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator error={query.error} label={t("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -177,7 +171,7 @@ const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps
|
||||
];
|
||||
|
||||
return (
|
||||
<ScrollArea h="100%" w="100%" type="auto" offsetScrollbars scrollbarSize={6}>
|
||||
<ScrollArea h="100%" w="100%">
|
||||
<Stack gap="md" p="md">
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||
<Stack gap={2}>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { storedContentToPlainText } from "./content";
|
||||
|
||||
@@ -239,7 +238,6 @@ const AnchorNoteWidgetContent = ({
|
||||
)}
|
||||
</Stack>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<WidgetQueryErrorIndicator error={noteQuery.error} label={t("name")} />
|
||||
{(isEditing || canEdit) && (
|
||||
<Group className="homarr-anchor-actions" data-visible={isEditing || isAdvanced || undefined} gap="xs">
|
||||
{isEditing ? (
|
||||
|
||||
@@ -16,7 +16,7 @@ import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getSafeAppHref, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./app.module.css";
|
||||
import { PingDot } from "./ping/ping-dot";
|
||||
@@ -24,7 +24,6 @@ import { PingIndicator } from "./ping/ping-indicator";
|
||||
|
||||
export default function AppWidget({ options, isEditMode, height, width }: WidgetComponentProps<"app">) {
|
||||
const tCommon = useI18n("common");
|
||||
const tApp = useI18n("widget.app");
|
||||
const settings = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
const appQuery = clientApi.app.byId.useQuery({ id: options.appId }, { enabled: Boolean(options.appId) });
|
||||
@@ -127,11 +126,6 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
</Suspense>
|
||||
) : null}
|
||||
</AppLink>
|
||||
{appQuery.error && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator error={appQuery.error} label={tApp("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { getIconUrl } from "@homarr/definitions";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
|
||||
export default function ArchiveTeamWarriorWidget({
|
||||
integrationIds,
|
||||
@@ -50,7 +49,7 @@ const ArchiveTeamWarriorWidgetContent = ({
|
||||
const t = useI18n("widget.archiveTeamWarrior");
|
||||
const tWidgetCommon = useI18n("widget.common");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const [data, statusQuery] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ integrationId });
|
||||
const [data] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ integrationId });
|
||||
|
||||
const status = data.status;
|
||||
const projectName = status.project?.title ?? status.selectedProject ?? t("noProjectSelected");
|
||||
@@ -74,7 +73,6 @@ const ArchiveTeamWarriorWidgetContent = ({
|
||||
</Group>
|
||||
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<WidgetQueryErrorIndicator error={statusQuery.error} label={t("name")} />
|
||||
<Badge size="sm" color={getStatusColor(statusKey)}>
|
||||
{t(`status.${statusKey}`)}
|
||||
</Badge>
|
||||
|
||||
@@ -6,11 +6,9 @@ import { getQueryKey } from "@trpc/react-query";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { formatBytes, formatDuration } from "@homarr/common";
|
||||
import type { AudiobookshelfDashboardData } from "@homarr/integrations/types";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import { AudioStatsContent } from "./audio-stats-content";
|
||||
@@ -23,8 +21,6 @@ export default function AudioStatsWidget({
|
||||
displayMode = "compact",
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"audioStats">) {
|
||||
const tAudio = useI18n("widget.audioStats");
|
||||
const tMediaServer = useI18n("widget.mediaServer");
|
||||
const statsInput = { integrationId: integrationIds[0] ?? "" };
|
||||
const { data: response, error: statsError } = clientApi.widget.audioStats.getStats.useQuery(statsInput);
|
||||
const streamsInput = { integrationIds: [statsInput.integrationId], showOnlyPlaying: true };
|
||||
@@ -56,9 +52,6 @@ export default function AudioStatsWidget({
|
||||
if (displayMode === "compact") {
|
||||
return (
|
||||
<Box h="100%" pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={statsError} label={tAudio("name")} />
|
||||
</Box>
|
||||
{summary}
|
||||
</Box>
|
||||
);
|
||||
@@ -73,10 +66,8 @@ export default function AudioStatsWidget({
|
||||
return (
|
||||
<Stack h="100%" gap="lg" p="md">
|
||||
<div style={{ minHeight: 150 }}>{summary}</div>
|
||||
{(statsError || streamsError || hasStreamErrors) && (
|
||||
{hasStreamErrors && (
|
||||
<Group justify="flex-end">
|
||||
<WidgetQueryErrorIndicator error={statsError} label={tAudio("name")} />
|
||||
<WidgetQueryErrorIndicator error={streamsError} label={tMediaServer("name")} />
|
||||
<IntegrationErrorIndicator results={currentStreams} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import { getBeszelAlertsQueryInput } from "./display";
|
||||
import { buildBeszelSystemNameMap, getBeszelSystemName } from "./system-name-map";
|
||||
@@ -126,7 +125,6 @@ export default function BeszelAlertsWidget({
|
||||
<Box h="100%" pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
|
||||
<Group gap={0}>
|
||||
<WidgetQueryErrorIndicator error={alertsQuery.error} label={t("name")} />
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
import { useBeszelFilteredSystems } from "../beszel/_shared/hooks";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal";
|
||||
import { DiskUsage } from "../beszel/_shared/disk-usage";
|
||||
import { isBeszelGridMetricVisible } from "./display";
|
||||
@@ -460,7 +459,6 @@ export default function BeszelSystemGridWidget({
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"beszelSystemGrid">) {
|
||||
const t = useI18n("widget.beszel");
|
||||
const tFeature = useI18n("widget.beszelSystemGrid");
|
||||
const tCommon = useI18n("common");
|
||||
const board = useRequiredBoard();
|
||||
const { openModal } = useModalAction(BeszelSystemStatsModal);
|
||||
@@ -485,7 +483,6 @@ export default function BeszelSystemGridWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={tFeature("name")} />
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
</Group>
|
||||
<Center h="100%">
|
||||
@@ -514,7 +511,6 @@ export default function BeszelSystemGridWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={tFeature("name")} />
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
</Group>
|
||||
<Box
|
||||
|
||||
@@ -19,7 +19,6 @@ import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import type { BeszelTimePeriod } from "../beszel/_shared/chart";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { BeszelStatsView } from "../beszel/_shared/stats-view";
|
||||
import { createBeszelSystemChoices, resolveBeszelSystemChoice } from "./selection";
|
||||
|
||||
@@ -144,7 +143,6 @@ export default function BeszelSystemStatsWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={t("name")} />
|
||||
<IntegrationErrorIndicator results={systemsResult} />
|
||||
</Group>
|
||||
<Center h="100%">
|
||||
@@ -184,7 +182,6 @@ export default function BeszelSystemStatsWidget({
|
||||
<Box h="100%" pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
|
||||
<Group gap={0}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={t("name")} />
|
||||
<IntegrationErrorIndicator results={systemsResult} />
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
} from "../beszel/_shared/format";
|
||||
import { useBeszelFilteredSystems } from "../beszel/_shared/hooks";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal";
|
||||
import { DiskUsage } from "../beszel/_shared/disk-usage";
|
||||
import { getBeszelTableVisibleMetricKeys } from "./display";
|
||||
@@ -453,7 +452,6 @@ export default function BeszelSystemTableWidget({
|
||||
<div style={{ position: "relative", height: "100%" }}>
|
||||
<div style={{ position: "absolute", top: 4, right: 8, zIndex: 1 }}>
|
||||
<Group gap={0}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={t("name")} />
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@ export const BookmarkAddButton: SortableItemListInput<BookmarkSelectionItem, str
|
||||
</Combobox.DropdownTarget>
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<ScrollArea.Autosize mah={280} type="auto">
|
||||
<ScrollArea.Autosize mah={280}>
|
||||
<Combobox.Options>
|
||||
{pendingUrl && !selectedValues.has(pendingUrl.id) ? (
|
||||
<Combobox.Option value={createOptionValue}>
|
||||
|
||||
@@ -148,11 +148,7 @@ export default function BookmarksWidget({
|
||||
</Center>
|
||||
) : (
|
||||
<ScrollArea
|
||||
type="hover"
|
||||
scrollbars={plan.horizontalScroll ? "x" : "y"}
|
||||
scrollbarSize={2}
|
||||
scrollHideDelay={500}
|
||||
offsetScrollbars
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
>
|
||||
<Box miw="100%" mih="100%" pb={2}>
|
||||
|
||||
@@ -56,7 +56,6 @@ export const CalendarEventList = ({
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars
|
||||
pt={5}
|
||||
w="100%"
|
||||
h={fillHeight ? "100%" : undefined}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { CalendarDay } from "./calender-day";
|
||||
@@ -54,7 +53,6 @@ export default function CalendarWidget(props: WidgetComponentProps<"calendar">)
|
||||
events={[]}
|
||||
failedIntegrations={[]}
|
||||
isPending={false}
|
||||
queryError={undefined}
|
||||
month={month}
|
||||
setMonth={setMonth}
|
||||
/>
|
||||
@@ -106,7 +104,6 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options, d
|
||||
events={events}
|
||||
failedIntegrations={failedIntegrations}
|
||||
isPending={isPending}
|
||||
queryError={calendarQuery.error}
|
||||
month={month}
|
||||
setMonth={setMonth}
|
||||
options={options}
|
||||
@@ -120,7 +117,6 @@ interface CalendarBaseProps {
|
||||
events: CalendarEventWithSource[];
|
||||
failedIntegrations: { integrationId: string; integrationName: string; error: string }[];
|
||||
isPending: boolean;
|
||||
queryError: unknown;
|
||||
month: Date;
|
||||
setMonth: (date: Date) => void;
|
||||
options: WidgetComponentProps<"calendar">["options"];
|
||||
@@ -132,13 +128,11 @@ const CalendarBase = ({
|
||||
events,
|
||||
failedIntegrations,
|
||||
isPending,
|
||||
queryError,
|
||||
month,
|
||||
setMonth,
|
||||
options,
|
||||
displayMode,
|
||||
}: CalendarBaseProps) => {
|
||||
const t = useI18n("widget.calendar");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const { firstDayOfWeek } = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
@@ -170,8 +164,6 @@ const CalendarBase = ({
|
||||
events={agendaEvents}
|
||||
failedIntegrations={failedIntegrations}
|
||||
isPending={isPending}
|
||||
queryError={queryError}
|
||||
queryErrorLabel={t("name")}
|
||||
isEditMode={isEditMode}
|
||||
locale={locale}
|
||||
month={month}
|
||||
@@ -180,8 +172,6 @@ const CalendarBase = ({
|
||||
);
|
||||
}
|
||||
|
||||
const hasErrors = failedIntegrations.length > 0 || Boolean(queryError);
|
||||
|
||||
return (
|
||||
<Box ref={ref} h="100%" w="100%" pos="relative" style={{ overflow: "hidden" }}>
|
||||
<Calendar
|
||||
@@ -194,13 +184,11 @@ const CalendarBase = ({
|
||||
date={month}
|
||||
maxLevel="month"
|
||||
firstDayOfWeek={firstDayOfWeek}
|
||||
static={isEditMode}
|
||||
className={classes.calendar}
|
||||
w="100%"
|
||||
h="100%"
|
||||
styles={{
|
||||
calendarHeaderControl: {
|
||||
pointerEvents: isEditMode ? "none" : undefined,
|
||||
borderRadius: "md",
|
||||
height: isSmall ? "1.5rem" : undefined,
|
||||
width: isSmall ? "1.5rem" : undefined,
|
||||
@@ -249,17 +237,16 @@ const CalendarBase = ({
|
||||
<CalendarDay
|
||||
date={dayjs(tileDate).toDate()}
|
||||
events={eventsForDate}
|
||||
disabled={isEditMode || eventsForDate.length === 0}
|
||||
disabled={eventsForDate.length === 0}
|
||||
rootWidth={width}
|
||||
rootHeight={height}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{hasErrors && (
|
||||
{failedIntegrations.length > 0 && (
|
||||
<Group className={classes.errorIndicator} gap={0} pos="absolute">
|
||||
<IntegrationErrorIndicator results={failedIntegrations} />
|
||||
<WidgetQueryErrorIndicator error={queryError} label={t("name")} />
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
@@ -270,8 +257,6 @@ interface CalendarAgendaProps {
|
||||
events: CalendarEventWithSource[];
|
||||
failedIntegrations: CalendarBaseProps["failedIntegrations"];
|
||||
isPending: boolean;
|
||||
queryError: unknown;
|
||||
queryErrorLabel: string;
|
||||
isEditMode: boolean;
|
||||
locale: string;
|
||||
month: Date;
|
||||
@@ -282,8 +267,6 @@ const CalendarAgenda = ({
|
||||
events,
|
||||
failedIntegrations,
|
||||
isPending,
|
||||
queryError,
|
||||
queryErrorLabel,
|
||||
isEditMode,
|
||||
locale,
|
||||
month,
|
||||
@@ -331,7 +314,6 @@ const CalendarAgenda = ({
|
||||
</Group>
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={failedIntegrations} />
|
||||
<WidgetQueryErrorIndicator error={queryError} label={queryErrorLabel} />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
|
||||
@@ -68,9 +68,6 @@ export const AdvancedClockView = ({
|
||||
<ScrollArea
|
||||
h="100%"
|
||||
w="100%"
|
||||
type="auto"
|
||||
offsetScrollbars
|
||||
scrollbarSize={6}
|
||||
aria-label={t("worldClock.advancedLabel")}
|
||||
>
|
||||
<Stack gap="md" p={{ base: "sm", sm: "md" }}>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { AnimatedWeatherIcon } from "../weather/animated-icon";
|
||||
import { WeatherDescription } from "../weather/icon";
|
||||
|
||||
@@ -25,17 +24,16 @@ export const ClockWeatherSummary = ({
|
||||
animateIcon,
|
||||
detailed,
|
||||
}: ClockWeatherSummaryProps) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const weatherQuery = clientApi.widget.weather.atLocation.useQuery({ latitude, longitude });
|
||||
const weather = weatherQuery.data;
|
||||
|
||||
if (!weather) {
|
||||
return isInitialWidgetQueryPending(weatherQuery) ? (
|
||||
<Loader size="xs" aria-label={tCommon("action.loading")} />
|
||||
) : (
|
||||
<WidgetQueryErrorIndicator error={weatherQuery.error} label={t("widget.weather.name")} />
|
||||
);
|
||||
if (isInitialWidgetQueryPending(weatherQuery)) {
|
||||
return <Loader size="xs" aria-label={tCommon("action.loading")} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const temperature = isFahrenheit ? weather.current.temperature * (9 / 5) + 32 : weather.current.temperature;
|
||||
@@ -72,7 +70,6 @@ export const ClockWeatherSummary = ({
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<WidgetQueryErrorIndicator error={weatherQuery.error} label={t("widget.weather.name")} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -22,6 +22,16 @@
|
||||
background-color: color-mix(in srgb, var(--mantine-color-default-hover) 40%, transparent) !important;
|
||||
}
|
||||
|
||||
.homarr-data-table .mantine-datatable-scroll-area-scrollbar[data-state="visible"]:hover {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-8);
|
||||
}
|
||||
}
|
||||
|
||||
.homarr-data-table th {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { DataTableColumn, DataTableProps } from "mantine-datatable";
|
||||
import { DataTable } from "mantine-datatable";
|
||||
|
||||
import { scrollAreaDefaultProps } from "@homarr/ui";
|
||||
|
||||
const fallbackColumnWidth = 60;
|
||||
|
||||
const getColumnWidth = (width: string | number | undefined): number => {
|
||||
@@ -149,11 +151,11 @@ export function HomarrDataTable<T>({
|
||||
defaultColumnProps={{
|
||||
...defaultColumnProps,
|
||||
noWrap: defaultColumnProps?.noWrap ?? true,
|
||||
draggable: !isEditMode,
|
||||
resizable: !isEditMode,
|
||||
draggable: true,
|
||||
resizable: true,
|
||||
cellsStyle: defaultColumnProps?.cellsStyle ?? (() => ({ padding: cellPadding })),
|
||||
}}
|
||||
scrollAreaProps={{ type: "auto", scrollbarSize: 6, ...scrollAreaProps }}
|
||||
scrollAreaProps={{ ...scrollAreaDefaultProps, ...scrollAreaProps }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,10 @@ import { createRoot } from "react-dom/client";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "./query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "./query-state-indicator";
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useI18n: () => (key: string, values?: { widget?: string }) => {
|
||||
if (key === "stale") return `Could not refresh ${values?.widget}; showing saved data`;
|
||||
useI18n: () => (key: string) => {
|
||||
if (key === "loading") return "Loading widget data";
|
||||
return key;
|
||||
},
|
||||
@@ -26,7 +25,7 @@ const matchMedia = (query: string) => ({
|
||||
dispatchEvent: () => false,
|
||||
});
|
||||
|
||||
describe("WidgetQueryErrorIndicator", () => {
|
||||
describe("WidgetQueryLoadingState", () => {
|
||||
let host: HTMLDivElement | undefined;
|
||||
let root: ReturnType<typeof createRoot> | undefined;
|
||||
|
||||
@@ -36,28 +35,6 @@ describe("WidgetQueryErrorIndicator", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("uses a localized label without exposing the raw failure", async () => {
|
||||
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
||||
vi.stubGlobal("matchMedia", matchMedia);
|
||||
host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
root = createRoot(host);
|
||||
const secret = "http://admin:password@private.local/path?token=secret";
|
||||
|
||||
await act(async () =>
|
||||
root?.render(
|
||||
createElement(
|
||||
MantineProvider,
|
||||
null,
|
||||
createElement(WidgetQueryErrorIndicator, { error: new Error(secret), label: "Weather" }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(host.querySelector("[aria-label='Could not refresh Weather; showing saved data']")).not.toBeNull();
|
||||
expect(host.textContent).not.toMatch(/password|private\.local|token=secret/);
|
||||
});
|
||||
|
||||
test("announces the loading state to assistive technology", async () => {
|
||||
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
||||
vi.stubGlobal("matchMedia", matchMedia);
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { ActionIcon, Center, Loader, Tooltip, VisuallyHidden } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { Center, Loader, VisuallyHidden } from "@mantine/core";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface WidgetQueryErrorIndicatorProps {
|
||||
error: unknown;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** Localized warning for stale or optional-query failures. Never renders the raw error. */
|
||||
export function WidgetQueryErrorIndicator({ error, label }: WidgetQueryErrorIndicatorProps) {
|
||||
const t = useI18n("common.widgetQuery");
|
||||
if (!error) return null;
|
||||
|
||||
const accessibleLabel = t("stale", { widget: label });
|
||||
return (
|
||||
<Tooltip label={accessibleLabel} position="left" withArrow>
|
||||
<ActionIcon
|
||||
component="span"
|
||||
color="orange"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
role="note"
|
||||
aria-label={accessibleLabel}
|
||||
tabIndex={0}
|
||||
style={{ cursor: "help" }}
|
||||
>
|
||||
<IconAlertTriangle aria-hidden size="var(--mantine-font-size-sm)" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function WidgetQueryLoadingState() {
|
||||
const t = useI18n("common.widgetQuery");
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
import { Badge, Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { createWidgetKey, getCoolifySectionVisibility } from "./coolify-utils";
|
||||
@@ -148,9 +146,6 @@ const FailedSourceBadges = ({
|
||||
{t("source.unavailable", { source: source.integrationName })}
|
||||
</Badge>
|
||||
))}
|
||||
<Box ml="auto">
|
||||
<WidgetQueryErrorIndicator error={queryError} label={getIntegrationName("coolify")} />
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -184,7 +184,7 @@ export default function CountdownWidget({
|
||||
onNotificationsChange={setNotifications}
|
||||
/>
|
||||
|
||||
<ScrollArea type="auto" style={{ flex: 1, minHeight: 0 }}>
|
||||
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
|
||||
<Stack gap="md" pb="xs">
|
||||
<EventSection
|
||||
title={t("upcoming")}
|
||||
|
||||
@@ -35,7 +35,6 @@ import type { widgetKind } from ".";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../../common/query-state-indicator";
|
||||
import actionTargetClasses from "../../common/action-target.module.css";
|
||||
import classes from "./component.module.css";
|
||||
import TimerModal from "./TimerModal";
|
||||
@@ -165,7 +164,6 @@ export default function DnsHoleControlsWidget({
|
||||
<Box pos="absolute" top={4} right={4} style={{ zIndex: 2 }}>
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={summaryResults} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
</Box>
|
||||
{controlAllButtonsVisible && (
|
||||
|
||||
@@ -32,7 +32,6 @@ import type { widgetKind } from ".";
|
||||
import type { WidgetComponentProps, WidgetProps } from "../../definition";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../../common/query-state-indicator";
|
||||
|
||||
export default function DnsHoleSummaryWidget({
|
||||
options,
|
||||
@@ -71,7 +70,6 @@ export default function DnsHoleSummaryWidget({
|
||||
<Box pos="absolute" top={4} right={4} style={{ zIndex: 2 }}>
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={summaries} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={tDns("name")} />
|
||||
</Group>
|
||||
</Box>
|
||||
<SimpleGrid cols={2} spacing="xs" p="xs" {...layoutProps} style={{ ...layoutProps.style, flex: 1, minHeight: 0 }}>
|
||||
@@ -104,7 +102,7 @@ export default function DnsHoleSummaryWidget({
|
||||
)}
|
||||
</SimpleGrid>
|
||||
{showSourceStatuses && (
|
||||
<ScrollArea type="auto" scrollbarSize={4} px="xs" pb="xs">
|
||||
<ScrollArea px="xs" pb="xs">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{successfulSummaries.map(({ integration, summary }) => (
|
||||
<Badge
|
||||
|
||||
@@ -55,7 +55,6 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { HomarrDataTable } from "../common/homarr-data-table";
|
||||
import { formatLocalizedDateTime } from "../common/locale";
|
||||
import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout";
|
||||
@@ -314,7 +313,6 @@ export default function DownloadClientsWidget({
|
||||
const queryIndicators = (
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={currentItems ?? []} />
|
||||
<WidgetQueryErrorIndicator error={downloadsQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Accordion, Box, Center, Flex, Group, RingProgress, Stack, Text } from "@mantine/core";
|
||||
import { Accordion, Center, Flex, Group, RingProgress, Stack, Text } from "@mantine/core";
|
||||
import { IconBrain, IconCpu, IconCube, IconDatabase, IconDeviceLaptop, IconServer } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -9,7 +9,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { formatUptime } from "../system-health";
|
||||
import { getClusterAccordionDefault, getClusterVisibleSections } from "./accordion-state";
|
||||
@@ -71,9 +70,6 @@ export const ClusterHealthMonitoring = ({
|
||||
const isTiny = displayMode !== "advanced" && width < 256;
|
||||
return (
|
||||
<Stack h={isAdvanced ? "auto" : "100%"} p="xs" gap={isTiny ? "xs" : "md"} pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
{(isAdvanced || options.showUptime) && !isTiny && (
|
||||
<Group justify="center" wrap="nowrap">
|
||||
<Text fz="xs" fw={700} c="dimmed" ta="center">
|
||||
|
||||
@@ -45,7 +45,7 @@ import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-stor
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { CpuRing } from "./rings/cpu-ring";
|
||||
@@ -81,7 +81,6 @@ export const SystemHealthMonitoring = ({
|
||||
const queryIndicators = (
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={healthResults} />
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { useWidgetRuntimeActions } from "../../runtime-hooks";
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../../common/query-state-indicator";
|
||||
import classes from "./component.module.css";
|
||||
import { ALL_PHOTOS_ALBUM_ID } from "./constants";
|
||||
|
||||
@@ -32,7 +31,6 @@ export default function ImmichAlbumCarouselWidget({
|
||||
}: WidgetComponentProps<"immich-albumCarousel">) {
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const t = useI18n("widget.immich-albumCarousel");
|
||||
|
||||
const albumQuery = clientApi.widget.immich.getAlbum.useQuery(
|
||||
{
|
||||
@@ -71,9 +69,6 @@ export default function ImmichAlbumCarouselWidget({
|
||||
|
||||
return (
|
||||
<Box h="100%" pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 3 }}>
|
||||
<WidgetQueryErrorIndicator error={albumQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
{album.assets.length === 0 || photoAssets.length === 0 ? (
|
||||
<NoPhotosInAlbum />
|
||||
) : (
|
||||
@@ -228,7 +223,7 @@ function Carousel({
|
||||
)}
|
||||
</Box>
|
||||
{advanced && (
|
||||
<ScrollArea type="never" scrollbarSize={0} px="xs" py={6}>
|
||||
<ScrollArea px="xs" py={6}>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
{assets.map((asset, index) => (
|
||||
<UnstyledButton
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Box, Center, Group, Progress, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
import { Group, Progress, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
import { IconDatabase, IconPhoto, IconUsers, IconVideo } from "@tabler/icons-react";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../../common/query-state-indicator";
|
||||
import { useWidgetRuntimeQueries } from "../../runtime-hooks";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
@@ -103,11 +103,6 @@ export default function ImmichServerStatsWidget({
|
||||
return (
|
||||
<Stack gap="md" h="100%" p={statsLayout.dense ? "xs" : "md"} justify="center" pos="relative">
|
||||
{statsContent}
|
||||
{statsQuery.error && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -121,15 +116,10 @@ export default function ImmichServerStatsWidget({
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("albumLimit", { count: MAX_ADVANCED_ALBUMS })}
|
||||
</Text>
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error ?? albumsQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
|
||||
{isInitialWidgetQueryPending(albumsQuery) ? (
|
||||
<WidgetQueryLoadingState />
|
||||
) : albumsQuery.error && albumsQuery.data === undefined ? (
|
||||
<Center h="100%" p="md">
|
||||
<WidgetQueryErrorIndicator error={albumsQuery.error} label={t("name")} />
|
||||
</Center>
|
||||
) : albums.length === 0 ? (
|
||||
<WidgetEmptyState />
|
||||
) : (
|
||||
|
||||
@@ -29,7 +29,6 @@ import { useWidgetRuntimeActions } from "../runtime-hooks";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import classes from "./component.module.css";
|
||||
import { getIndexerDisplayStatus } from "./status";
|
||||
@@ -118,7 +117,6 @@ export default function IndexerManagerWidget({
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<WidgetQueryErrorIndicator error={indexersQuery.error} label={t("title")} />
|
||||
<Tooltip label={testAllError ? tCommon("error") : t("testAll")}>
|
||||
<ActionIcon
|
||||
className={combineClasses("indexer-manager-test-action-icon", classes.testAction, actionTargetClasses.root)}
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import { NoIntegrationDataError } from "../errors/no-data-integration";
|
||||
@@ -126,7 +125,6 @@ export default function MediaMissingWidget({
|
||||
) => (
|
||||
<ScrollArea
|
||||
h="100%"
|
||||
scrollbarSize={4}
|
||||
startScrollPosition={tabScrollPositionsRef.current[tab]}
|
||||
onScrollPositionChange={(position) => {
|
||||
tabScrollPositionsRef.current[tab] = position;
|
||||
@@ -164,15 +162,10 @@ export default function MediaMissingWidget({
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
const queryFailure = <WidgetQueryErrorIndicator error={mediaQuery.error} label={t("name")} />;
|
||||
|
||||
if (isAdvanced) {
|
||||
return (
|
||||
<Stack h="100%" gap={0}>
|
||||
<Group justify="flex-end" px="xs">
|
||||
{queryFailure}
|
||||
{partialFailures}
|
||||
</Group>
|
||||
{partialFailures}
|
||||
<SimpleGrid cols={enabledPanelCount} spacing="sm" p="sm" style={{ flex: 1, minHeight: 0 }}>
|
||||
{showMissing && (
|
||||
<Paper withBorder radius="sm" style={{ minHeight: 0, overflow: "hidden" }}>
|
||||
@@ -211,10 +204,7 @@ export default function MediaMissingWidget({
|
||||
h="100%"
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<Group justify="flex-end" px="xs">
|
||||
{queryFailure}
|
||||
{partialFailures}
|
||||
</Group>
|
||||
{partialFailures}
|
||||
<Tabs.List grow>
|
||||
{showMissing && (
|
||||
<Tabs.Tab
|
||||
|
||||
@@ -33,7 +33,7 @@ import type { WidgetComponentProps } from "../definition";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function MediaReleasesWidget({
|
||||
@@ -43,7 +43,6 @@ export default function MediaReleasesWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"mediaReleases">) {
|
||||
const t = useI18n("widget.mediaReleases");
|
||||
const releasesQuery = clientApi.widget.mediaRelease.getMediaReleases.useQuery({ integrationIds });
|
||||
const response = getUsableWidgetQueryData(releasesQuery);
|
||||
|
||||
@@ -70,10 +69,9 @@ export default function MediaReleasesWidget({
|
||||
</SimpleGrid>
|
||||
</ScrollArea>
|
||||
)}
|
||||
{(failedIntegrations.length > 0 || releasesQuery.error) && (
|
||||
{failedIntegrations.length > 0 && (
|
||||
<Group pos="absolute" top={4} right={4} gap={0}>
|
||||
<IntegrationErrorIndicator results={failedIntegrations} />
|
||||
<WidgetQueryErrorIndicator error={releasesQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -61,7 +61,6 @@ export default function MediaServerWidget({
|
||||
)}
|
||||
<ScrollArea
|
||||
className="mediaRequests-list-scrollArea"
|
||||
scrollbarSize="md"
|
||||
style={{ flex: 1, minHeight: 0, pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
|
||||
|
||||
@@ -40,7 +40,6 @@ import type { TablerIcon } from "@homarr/ui";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
type TranscodingDecision = NonNullable<NonNullable<StreamSession["currentlyPlaying"]>["metadata"]>["transcoding"];
|
||||
@@ -262,14 +261,12 @@ export default function MediaServerWidget({
|
||||
aria-label={tSearch("placeholder")}
|
||||
leftSection={<IconSearch size="var(--mantine-font-size-xs)" aria-hidden />}
|
||||
/>
|
||||
<WidgetQueryErrorIndicator error={currentStreamsQuery.error} label={t("name")} />
|
||||
<IntegrationErrorIndicator results={currentStreams} />
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
{!isAdvanced && (hasFailedIntegrations || currentStreamsQuery.error) && (
|
||||
{!isAdvanced && hasFailedIntegrations && (
|
||||
<Group px="xs" justify="flex-end">
|
||||
<WidgetQueryErrorIndicator error={currentStreamsQuery.error} label={t("name")} />
|
||||
<IntegrationErrorIndicator results={currentStreams} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
@@ -26,7 +25,6 @@ import { views } from ".";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { useWidgetRuntimeQueries } from "../runtime-hooks";
|
||||
import { HealthCheckStatus } from "./health-check-status";
|
||||
import { QueuePanel } from "./panels/queue.panel";
|
||||
@@ -115,16 +113,9 @@ export default function MediaTranscodingWidget({
|
||||
const queuePageCount = totalQueuePages ?? 1;
|
||||
const isTiny = !isAdvanced && (width < 280 || height < 140);
|
||||
const footerLayout = getTranscodingFooterLayout(width, height);
|
||||
const queryIndicator = (
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
|
||||
<WidgetQueryErrorIndicator error={transcodingQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (isAdvanced) {
|
||||
return (
|
||||
<Stack gap="xs" h="100%" p="xs" pos="relative">
|
||||
{queryIndicator}
|
||||
<ScrollArea h="100%" style={{ flex: 1 }}>
|
||||
<SimpleGrid cols={widthLayout.advancedColumns} spacing="sm">
|
||||
<AdvancedPanel title={t("tab.workers")} icon={IconCpu2}>
|
||||
@@ -155,7 +146,6 @@ export default function MediaTranscodingWidget({
|
||||
|
||||
return (
|
||||
<Stack gap={4} h="100%" pos="relative">
|
||||
{queryIndicator}
|
||||
{view === "workers" ? (
|
||||
<WorkersPanel workers={transcodingData.data.workers} isTiny={isTiny} />
|
||||
) : view === "queue" ? (
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../../common/query-state-indicator";
|
||||
import { getNetworkControllerStatusLayout } from "./layout";
|
||||
import { WifiVariant } from "./variants/wifi-variant";
|
||||
import { WiredVariant } from "./variants/wired-variant";
|
||||
@@ -35,7 +34,6 @@ export default function NetworkControllerNetworkStatusWidget({
|
||||
),
|
||||
[results],
|
||||
);
|
||||
const t = useI18n("widget.networkControllerStatus");
|
||||
const tWidgetCommon = useI18n("widget.common");
|
||||
const tCommon = useI18n("common");
|
||||
const locale = useCurrentIntlLocale();
|
||||
@@ -49,7 +47,6 @@ export default function NetworkControllerNetworkStatusWidget({
|
||||
const queryIndicators = (
|
||||
<Group gap={0} justify="flex-end">
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { WidgetComponentProps } from "../../definition";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
import { getBinaryStatusKey } from "../../common/locale";
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../../common/query-state-indicator";
|
||||
import type { NetworkControllerMatrixMetricKey, NetworkControllerMatrixSectionKey } from "./display";
|
||||
import { getNetworkControllerMatrix } from "./display";
|
||||
|
||||
@@ -57,7 +56,6 @@ export default function NetworkControllerSummaryWidget({
|
||||
const queryIndicators = (
|
||||
<Group gap={0} justify="flex-end">
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
|
||||
@@ -500,7 +500,6 @@ export function Notebook({
|
||||
|
||||
<ScrollArea
|
||||
mih="4rem"
|
||||
offsetScrollbars
|
||||
pl={12}
|
||||
pt={12}
|
||||
styles={{
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getNotificationDisplay } from "./display";
|
||||
import classes from "./component.module.css";
|
||||
@@ -67,11 +66,6 @@ export default function NotificationsWidget({
|
||||
return (
|
||||
<ScrollArea className="scroll-area-w100" w="100%" h="100%" p="xs">
|
||||
<Stack w="100%" gap="xs">
|
||||
{notificationsQuery.error && (
|
||||
<Group justify="flex-end">
|
||||
<WidgetQueryErrorIndicator error={notificationsQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
)}
|
||||
{failedIntegrations.length > 0 && (
|
||||
<Group gap={4} wrap="wrap">
|
||||
{failedIntegrations.map((integration) => (
|
||||
|
||||
@@ -29,13 +29,6 @@
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.queryIndicator {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.compactRoot {
|
||||
padding: clamp(4px, 3cqi, 8px);
|
||||
gap: 2px;
|
||||
|
||||
@@ -16,13 +16,12 @@ import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./component.module.css";
|
||||
import { getGridCols, getVisiblePatchMonStatKeys, shouldShowComplianceHeroText } from "./layout-utils";
|
||||
@@ -278,9 +277,6 @@ export default function PatchMonWidget({
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.queryIndicator}>
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error} label={getIntegrationName("patchmon")} />
|
||||
</div>
|
||||
{showHero && (
|
||||
<ComplianceHero
|
||||
compliancePercent={compliancePercent}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { AveragesSection } from "./averages";
|
||||
import { combineSpeedtestDashboards, getAvailableSpeedtestDashboards, getCompactSections } from "./helpers";
|
||||
@@ -72,7 +71,6 @@ export default function SpeedtestTrackerWidget({
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="md" p="md">
|
||||
@@ -131,7 +129,6 @@ export default function SpeedtestTrackerWidget({
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
<Stack h="100%" gap="sm" p="xs" style={{ overflow: "hidden" }}>
|
||||
{dashboardData.length > 1 && (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
function round(value: number) {
|
||||
@@ -196,11 +196,6 @@ export default function StockPriceWidget({
|
||||
return (
|
||||
<Box h="100%" w="100%" pos="relative">
|
||||
{content}
|
||||
{stockQuery.error && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator error={stockQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes";
|
||||
import { NoIntegrationDataError } from "../errors/no-data-integration";
|
||||
@@ -253,7 +253,6 @@ export default function SystemResources({
|
||||
const queryIndicators = (
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
const emptyState = (
|
||||
|
||||
@@ -4,13 +4,11 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box, Center, Group, Loader, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { CombinedNetworkTrafficChart } from "./chart/combined-network-traffic";
|
||||
import { SystemResourceCPUChart } from "./chart/cpu-chart";
|
||||
import { SystemResourceGPUChart } from "./chart/gpu-chart";
|
||||
@@ -84,7 +82,6 @@ export default function SystemResources({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"systemResources">) {
|
||||
const t = useI18n("widget.systemResources");
|
||||
const healthQuery = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ integrationIds });
|
||||
const results = getUsableWidgetQueryData(healthQuery) ?? [];
|
||||
const data = results.filter(
|
||||
@@ -98,7 +95,6 @@ export default function SystemResources({
|
||||
const queryIndicators = (
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ const AdvancedTimer = ({
|
||||
const t = useI18n("widget.timer");
|
||||
const phaseLabel = getTimerPhaseLabel(runtime, t);
|
||||
return (
|
||||
<ScrollArea h="100%" w="100%" type="auto" offsetScrollbars scrollbarSize={6}>
|
||||
<ScrollArea h="100%" w="100%">
|
||||
<Stack gap="md" p="md">
|
||||
<Paper
|
||||
withBorder
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
import { Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import type { TracearrDashboardData } from "@homarr/integrations/types";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { NoIntegrationDataError } from "../errors/no-data-integration";
|
||||
import { formatTotalTracearrBitrate } from "./bitrate";
|
||||
import { RecentActivityList } from "./recent-activity-section";
|
||||
@@ -186,7 +184,6 @@ function TracearrContent({ integrationIds, options, width, height, displayMode }
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("tracearr")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
<SimpleGrid
|
||||
@@ -223,7 +220,6 @@ function TracearrContent({ integrationIds, options, width, height, displayMode }
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("tracearr")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="xs" p="xs">
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import type { TraefikDashboardData, TraefikProtocolSummary, TraefikResourceSummary } from "@homarr/integrations/types";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -20,7 +19,6 @@ import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { NoIntegrationDataError } from "../errors/no-data-integration";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
@@ -212,7 +210,6 @@ function TraefikWidgetContent({
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={data} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("traefik")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">{summary}</ScrollArea>
|
||||
</Box>
|
||||
@@ -265,7 +262,6 @@ function TraefikWidgetContent({
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={data} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("traefik")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="md" p="md">
|
||||
@@ -281,7 +277,6 @@ function TraefikWidgetContent({
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={data} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("traefik")} />
|
||||
</Group>
|
||||
<SimpleGrid cols={2} spacing="md" h="100%" p="md" style={{ gridTemplateRows: "minmax(0, 1fr)" }}>
|
||||
<ScrollArea h="100%">{summary}</ScrollArea>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
}
|
||||
|
||||
.chartSurface :global(.mantine-ScrollArea-viewport),
|
||||
.chartSurface :global(.mantine-ScrollArea-scrollbar),
|
||||
.chartSurface :global(.recharts-wrapper),
|
||||
.chartSurface :global(.recharts-surface) {
|
||||
background-color: transparent !important;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list
|
||||
import { formatTimeFrameLabel, formatXLabel, umamiQueryOptions } from "./umami-utils";
|
||||
import type { TimeFrame } from "./umami-utils";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
|
||||
interface UmamiContentProps {
|
||||
integrationIds: string[];
|
||||
@@ -270,14 +270,6 @@ export function UmamiContent({
|
||||
<Box className={classes.chartSurface} mt={4} style={{ flex: 1, minHeight: 0 }}>
|
||||
{displayMode === "advanced" ? advancedContent : selectedView}
|
||||
</Box>
|
||||
{(statsQuery.error || activeVisitorsQuery.error || multiEventQuery.error) && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator
|
||||
error={statsQuery.error ?? activeVisitorsQuery.error ?? multiEventQuery.error}
|
||||
label={t("name")}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { NoIntegrationSelectedError } from "../errors/no-integration-selected";
|
||||
|
||||
const statusColors: Record<UpsStatus, string> = {
|
||||
@@ -87,7 +86,6 @@ function UpsContent({ integrationIds, options, width, height, displayMode }: Ups
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={data} />
|
||||
<WidgetQueryErrorIndicator error={summariesQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
<WidgetEmptyState />
|
||||
</Box>
|
||||
@@ -118,7 +116,6 @@ function UpsContent({ integrationIds, options, width, height, displayMode }: Ups
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={data} />
|
||||
<WidgetQueryErrorIndicator error={summariesQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
{displayMode === "advanced" ? (
|
||||
|
||||
@@ -6,14 +6,12 @@ import { IconArrowDown, IconArrowUp, IconClockPause, IconServer } from "@tabler/
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { formatNumber } from "@homarr/common";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import { NoIntegrationDataError } from "../errors/no-data-integration";
|
||||
import classes from "./component.module.css";
|
||||
import { aggregateUptimeKumaDashboards } from "./aggregate";
|
||||
@@ -254,7 +252,6 @@ function UptimeKumaContent({ integrationIds, options, width, height, displayMode
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("uptimeKuma")} />
|
||||
</Group>
|
||||
{summaryContent}
|
||||
</Box>
|
||||
@@ -291,7 +288,6 @@ function UptimeKumaContent({ integrationIds, options, width, height, displayMode
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("uptimeKuma")} />
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="lg" p="md">
|
||||
@@ -307,7 +303,6 @@ function UptimeKumaContent({ integrationIds, options, width, height, displayMode
|
||||
<Box h="100%" pos="relative">
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||
<IntegrationErrorIndicator results={dashboardData} />
|
||||
<WidgetQueryErrorIndicator error={dashboardQuery.error} label={getIntegrationName("uptimeKuma")} />
|
||||
</Group>
|
||||
<SimpleGrid cols={2} spacing="lg" h="100%" p="md" style={{ gridTemplateRows: "minmax(0, 1fr)" }}>
|
||||
{summaryContent}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function VpnWidget({ options, integrationIds, width, height }: Wi
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="scroll-area-w100" w="100%" h="100%" offsetScrollbars p="xs">
|
||||
<ScrollArea className="scroll-area-w100" w="100%" h="100%" p="xs">
|
||||
<SimpleGrid cols={width >= 640 ? 2 : 1} spacing="sm">
|
||||
{integrations.map((result) => (
|
||||
<Stack key={result.integration.id} gap={4}>
|
||||
|
||||
@@ -143,7 +143,7 @@ export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWea
|
||||
const ticks = hourly.filter((_, index) => index % layout.hourlyTickStep === 0).map((hour) => hour.observedAt);
|
||||
|
||||
return (
|
||||
<ScrollArea h="100%" w="100%" type="auto" offsetScrollbars scrollbarSize={6}>
|
||||
<ScrollArea h="100%" w="100%">
|
||||
<Stack className={classes.advancedRoot} gap="md" p="md">
|
||||
<Group justify="space-between" align="flex-start" gap="md" wrap="wrap">
|
||||
<Group gap="md" wrap="nowrap">
|
||||
|
||||
@@ -110,10 +110,6 @@
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
|
||||
.errorIndicator {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (any-pointer: coarse) {
|
||||
.detailsButton,
|
||||
.forecastButton,
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { AdvancedWeather } from "./advanced";
|
||||
import classes from "./component.module.css";
|
||||
@@ -20,7 +19,6 @@ export default function WeatherWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"weather">) {
|
||||
const t = useI18n("widget.weather");
|
||||
const input = {
|
||||
latitude: options.location.latitude,
|
||||
longitude: options.location.longitude,
|
||||
@@ -40,11 +38,6 @@ export default function WeatherWidget({
|
||||
<CompactWeather height={height} isEditMode={isEditMode} options={options} weather={weather} width={width} />
|
||||
)}
|
||||
</Box>
|
||||
{weatherQuery.error && (
|
||||
<Box pos="absolute" top={4} right={4} className={classes.errorIndicator}>
|
||||
<WidgetQueryErrorIndicator error={weatherQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
@@ -41,7 +40,7 @@ const WudWidgetContent = ({
|
||||
isAdvanced: boolean;
|
||||
}) => {
|
||||
const t = useI18n("widget.wud");
|
||||
const [data, statsQuery] = clientApi.widget.wud.getStats.useSuspenseQuery({ integrationId });
|
||||
const [data] = clientApi.widget.wud.getStats.useSuspenseQuery({ integrationId });
|
||||
const board = useRequiredBoard();
|
||||
|
||||
const isTiny = !isAdvanced && width < 256;
|
||||
@@ -115,11 +114,6 @@ const WudWidgetContent = ({
|
||||
|
||||
return (
|
||||
<Stack p="xs" gap="xs" h="100%">
|
||||
{Boolean(statsQuery.error) && (
|
||||
<Group justify="flex-end">
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
)}
|
||||
{showTitle && !isTiny && (
|
||||
<Group gap="xs" wrap="nowrap" justify="space-between" miw={0}>
|
||||
<Group gap="xs" wrap="nowrap" miw={0}>
|
||||
|
||||
Reference in new issue
Block a user