diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx
index ee611b313..a33ec44cf 100644
--- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx
+++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx
@@ -34,6 +34,7 @@ import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { useSession } from "@homarr/auth/client";
import { env } from "@homarr/common/env";
import { showWarningNotification } from "@homarr/notifications";
+import { DemoReadOnlyProvider } from "@homarr/widgets/demo-read-only";
import { widgetQueryRefetchIntervals } from "@homarr/widgets/refetch-intervals";
import { useAuthContext } from "./session";
@@ -66,7 +67,7 @@ const constructWebsocketUrl = () => {
return `${getWebSocketProtocol()}://${window.location.hostname}:${window.location.port}/websockets`;
};
-export function TRPCReactProvider({ children }: PropsWithChildren) {
+export function TRPCReactProvider({ children, demoReadOnly }: PropsWithChildren<{ demoReadOnly: boolean }>) {
const { data: session } = useSession();
const { logoutRedirectInProgress } = useAuthContext();
const sessionQueryScope = getSessionQueryScope(session);
@@ -81,7 +82,7 @@ export function TRPCReactProvider({ children }: PropsWithChildren) {
currentScope={sessionQueryScope}
onScopeChange={handleScopeChange}
>
- {children}
+ {children}
);
}
@@ -102,7 +103,7 @@ const clearLegacyDashboardPersistence = () => {
}
};
-const ScopedTRPCReactProvider = ({ children }: PropsWithChildren) => {
+const ScopedTRPCReactProvider = ({ children, demoReadOnly }: PropsWithChildren<{ demoReadOnly: boolean }>) => {
useEffect(clearLegacyDashboardPersistence, []);
const wsClient = useMemo(
() =>
@@ -150,21 +151,26 @@ const ScopedTRPCReactProvider = ({ children }: PropsWithChildren) => {
},
});
client.setQueryDefaults([["widget"]], {
- refetchInterval: queryCacheDefaultRefetchIntervalMs,
+ refetchInterval: demoReadOnly ? false : queryCacheDefaultRefetchIntervalMs,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
for (const queryDefaults of widgetQueryRefetchIntervals) {
const policy: { refetchInterval?: number | false; staleTime?: number } = {};
- if (queryDefaults.intervalSeconds === null) policy.refetchInterval = false;
- if (typeof queryDefaults.intervalSeconds === "number") {
+ if (demoReadOnly || queryDefaults.intervalSeconds === null) policy.refetchInterval = false;
+ else if (typeof queryDefaults.intervalSeconds === "number") {
policy.refetchInterval = queryDefaults.intervalSeconds * 1000;
}
if ("staleTimeSeconds" in queryDefaults) policy.staleTime = queryDefaults.staleTimeSeconds * 1000;
client.setQueryDefaults(queryDefaults.queryKey, policy);
}
for (const { queryKey, ...policy } of dashboardSupportingQueryPolicies) {
- client.setQueryDefaults(queryKey, policy);
+ client.setQueryDefaults(
+ queryKey,
+ demoReadOnly
+ ? { ...policy, refetchInterval: false, refetchOnWindowFocus: false, refetchOnReconnect: false }
+ : policy,
+ );
}
if (typeof window === "undefined") {
client.getQueryCache().subscribe((event) => {
@@ -239,7 +245,7 @@ const ScopedTRPCReactProvider = ({ children }: PropsWithChildren) => {
},
}}
>
- {children}
+ {children}
{process.env.NODE_ENV === "development" && }
diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index 755d06b5a..8e4757d91 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -167,7 +167,7 @@ export default async function Layout(props: {
/>
),
(innerProps) => ,
- (innerProps) => ,
+ (innerProps) => ,
(innerProps) => ,
(innerProps) => ,
(innerProps) => (
diff --git a/packages/widgets/package.json b/packages/widgets/package.json
index 6ac7d70e9..251d9bf3a 100644
--- a/packages/widgets/package.json
+++ b/packages/widgets/package.json
@@ -19,6 +19,7 @@
"./custom-api/component": "./src/custom-api/component.tsx",
"./custom-api/custom-jsx-display": "./src/custom-api/custom-jsx-display.tsx",
"./custom-api/extract-display-data": "./src/custom-api/extract-display-data.ts",
+ "./demo-read-only": "./src/demo-read-only.tsx",
"./assistant/context": "./src/assistant/context.tsx",
"./catalog": "./src/catalog.ts",
"./definition": "./src/definition.ts",
diff --git a/packages/widgets/src/beszel/_shared/stats-view.tsx b/packages/widgets/src/beszel/_shared/stats-view.tsx
index dfb5d7915..93ddaed28 100644
--- a/packages/widgets/src/beszel/_shared/stats-view.tsx
+++ b/packages/widgets/src/beszel/_shared/stats-view.tsx
@@ -28,6 +28,7 @@ import {
import { createByteChartAxisFormatters, formatPercent } from "./format";
import { makeTooltipProps } from "./tooltip";
import { useLiveStats } from "./use-live-stats";
+import { useDemoReadOnly } from "../../demo-read-only";
const CHART_HEIGHT = 180;
const MEBIBYTE = 1024 * 1024;
@@ -97,9 +98,10 @@ export function BeszelStatsView({
);
const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork;
const isLive = timePeriod === "1m";
+ const demoReadOnly = useDemoReadOnly();
const historicalQuery = clientApi.widget.beszel.getSystemStats.useQuery(
{ integrationIds, systemId, timePeriod, includeDocker: showDocker },
- { refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemId !== "" },
+ { refetchInterval: isLive || demoReadOnly ? false : 5_000, enabled: !isLive && systemId !== "" },
);
const historicalData = isLive ? historicalQuery.data : getUsableWidgetQueryData(historicalQuery);
const { data: liveData, error: liveError } = useLiveStats(integrationIds, systemId, isLive && systemId !== "");
diff --git a/packages/widgets/src/custom-api/component.tsx b/packages/widgets/src/custom-api/component.tsx
index 0ce693ee3..8e5f104ea 100644
--- a/packages/widgets/src/custom-api/component.tsx
+++ b/packages/widgets/src/custom-api/component.tsx
@@ -11,6 +11,7 @@ import { Link, zoomCompensatedSize } from "@homarr/ui";
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
import type { WidgetComponentProps } from "../definition";
+import { useDemoReadOnly } from "../demo-read-only";
import { isLegacyCustomWidgetMigrationError, isTerminalCustomWidgetDefinitionError } from "./migration-state";
const CustomJsxDisplay = dynamic(() => import("./custom-jsx-display"), { ssr: false });
@@ -23,6 +24,7 @@ export default function CustomApiWidget({
}: WidgetComponentProps<"customApi">) {
const t = useI18n("widget.customApi");
const tCustomJsx = useI18n("widget.customApi.customJsx");
+ const demoReadOnly = useDemoReadOnly();
const { data: session } = useSession();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const definitionId = options.definitionId;
@@ -33,8 +35,9 @@ export default function CustomApiWidget({
{
enabled: Boolean(itemId) && Boolean(definitionId),
refetchInterval: (currentQuery) =>
- isTerminalCustomWidgetDefinitionError(currentQuery.state.error) ? false : intervalMs,
- retry: (failureCount, error) => !isTerminalCustomWidgetDefinitionError(error) && failureCount < 3,
+ demoReadOnly || isTerminalCustomWidgetDefinitionError(currentQuery.state.error) ? false : intervalMs,
+ retry: (failureCount, error) =>
+ !demoReadOnly && !isTerminalCustomWidgetDefinitionError(error) && failureCount < 3,
},
);
diff --git a/packages/widgets/src/demo-read-only.tsx b/packages/widgets/src/demo-read-only.tsx
new file mode 100644
index 000000000..2e6470b70
--- /dev/null
+++ b/packages/widgets/src/demo-read-only.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+const DemoReadOnlyContext = createContext(false);
+
+export const DemoReadOnlyProvider = DemoReadOnlyContext.Provider;
+
+export const useDemoReadOnly = () => useContext(DemoReadOnlyContext);
diff --git a/packages/widgets/src/stats/component.tsx b/packages/widgets/src/stats/component.tsx
index 79d2b07a3..9d5b5b9b7 100644
--- a/packages/widgets/src/stats/component.tsx
+++ b/packages/widgets/src/stats/component.tsx
@@ -22,6 +22,7 @@ import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
+import { useDemoReadOnly } from "../demo-read-only";
import { useWidgetRuntimeActions } from "../runtime-hooks";
import { statsEntriesSchema } from "./config";
import { StatsTable } from "./table";
@@ -41,6 +42,7 @@ export default function StatsWidget({
displayScale = 1,
}: WidgetComponentProps<"stats">) {
const board = useRequiredBoard();
+ const demoReadOnly = useDemoReadOnly();
const advanced = displayMode === "advanced";
const t = useI18n("widget.stats");
const utils = clientApi.useUtils();
@@ -81,8 +83,8 @@ export default function StatsWidget({
{ integrationId },
{
staleTime: 30_000,
- refetchInterval: 60_000,
- refetchOnWindowFocus: true,
+ refetchInterval: demoReadOnly ? false : 60_000,
+ refetchOnWindowFocus: !demoReadOnly,
refetchIntervalInBackground: false,
retry: false,
},
@@ -153,13 +155,13 @@ export default function StatsWidget({
});
useEffect(() => {
- if (document.hidden) return;
+ if (demoReadOnly || document.hidden) return;
for (const [index, integrationId] of visibleIds.entries()) {
const snapshot = snapshots[index];
if (!snapshot?.data || snapshot.error) continue;
if (snapshot.data.updatedAt === null && snapshot.data.retryAt <= Date.now()) void refresh(integrationId, false);
}
- }, [visibleIds, snapshots, refresh]);
+ }, [demoReadOnly, visibleIds, snapshots, refresh]);
const tableEntries = advanced
? ids.flatMap((id) => {
diff --git a/packages/widgets/src/umami/umami-content.tsx b/packages/widgets/src/umami/umami-content.tsx
index 494a69980..f09fc87df 100644
--- a/packages/widgets/src/umami/umami-content.tsx
+++ b/packages/widgets/src/umami/umami-content.tsx
@@ -12,7 +12,7 @@ import classes from "./component.module.css";
import { UmamiEventsContent } from "./umami-events-content";
import { getUmamiLayout } from "./umami-layout";
import { UmamiTopPagesContent, UmamiTopReferrersContent } from "./umami-top-list";
-import { formatTimeFrameLabel, formatXLabel, umamiQueryOptions } from "./umami-utils";
+import { formatTimeFrameLabel, formatXLabel, useUmamiQueryOptions } from "./umami-utils";
import type { TimeFrame } from "./umami-utils";
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
@@ -48,6 +48,7 @@ export function UmamiContent({
}: UmamiContentProps) {
const t = useI18n("widget.umami");
const locale = useCurrentIntlLocale();
+ const umamiQueryOptions = useUmamiQueryOptions();
const tickColor = "var(--mantine-color-dimmed)";
const statsQuery = clientApi.widget.umami.getVisitorStats.useQuery(
diff --git a/packages/widgets/src/umami/umami-top-list.tsx b/packages/widgets/src/umami/umami-top-list.tsx
index ffb060134..e426558f3 100644
--- a/packages/widgets/src/umami/umami-top-list.tsx
+++ b/packages/widgets/src/umami/umami-top-list.tsx
@@ -7,7 +7,7 @@ import type { UmamiMetricItem } from "@homarr/integrations/types";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import classes from "./component.module.css";
-import { umamiQueryOptions } from "./umami-utils";
+import { useUmamiQueryOptions } from "./umami-utils";
import { getUsableWidgetQueryData } from "../common/query-state";
interface UmamiTopListProps {
@@ -19,6 +19,7 @@ interface UmamiTopListProps {
export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
const t = useI18n("widget.umami");
+ const umamiQueryOptions = useUmamiQueryOptions();
const data =
getUsableWidgetQueryData(
clientApi.widget.umami.getTopPages.useQuery(
@@ -31,6 +32,7 @@ export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, lim
export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) {
const t = useI18n("widget.umami");
+ const umamiQueryOptions = useUmamiQueryOptions();
const data =
getUsableWidgetQueryData(
clientApi.widget.umami.getTopReferrers.useQuery(
diff --git a/packages/widgets/src/umami/umami-utils.ts b/packages/widgets/src/umami/umami-utils.ts
index d87881f86..2d4a8d674 100644
--- a/packages/widgets/src/umami/umami-utils.ts
+++ b/packages/widgets/src/umami/umami-utils.ts
@@ -1,8 +1,11 @@
import type { useI18n } from "@homarr/translation/client";
+import { useDemoReadOnly } from "../demo-read-only";
import { timeFrameValues } from "./index";
-export const umamiQueryOptions = { refetchInterval: 60_000 } as const;
+export const useUmamiQueryOptions = (): { refetchInterval: number | false } => ({
+ refetchInterval: useDemoReadOnly() ? false : 60_000,
+});
export const EVENT_COLORS = ["blue.5", "orange.5", "green.5", "red.5", "violet.5", "teal.5", "yellow.5", "pink.5"];