@rapidmx/web-client 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/apps/admin/diagnostics/index.tsx +15 -0
  2. package/apps/shared/components/admin/diagnostics/Badge.tsx +37 -0
  3. package/apps/shared/components/admin/diagnostics/ComponentsTable.tsx +131 -0
  4. package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +173 -0
  5. package/apps/shared/components/admin/diagnostics/HostCard.tsx +108 -0
  6. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +136 -0
  7. package/apps/shared/components/admin/diagnostics/KubernetesNotice.tsx +25 -0
  8. package/apps/shared/components/admin/diagnostics/LogStore.ts +75 -0
  9. package/apps/shared/components/admin/diagnostics/LogViewer.tsx +87 -0
  10. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +235 -0
  11. package/apps/shared/components/admin/diagnostics/MetricTile.tsx +43 -0
  12. package/apps/shared/components/admin/diagnostics/NamespaceCard.tsx +95 -0
  13. package/apps/shared/components/admin/diagnostics/PackagesTable.tsx +107 -0
  14. package/apps/shared/components/admin/diagnostics/ProcessCard.tsx +70 -0
  15. package/apps/shared/components/admin/diagnostics/PvcTable.tsx +79 -0
  16. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +110 -0
  17. package/apps/shared/components/admin/diagnostics/ServerVersionCard.tsx +37 -0
  18. package/apps/shared/components/admin/diagnostics/Sparkline.tsx +79 -0
  19. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +81 -0
  20. package/apps/shared/components/admin/diagnostics/UsageMeter.tsx +73 -0
  21. package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +36 -0
  22. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +197 -0
  23. package/apps/shared/components/admin/diagnostics/download.ts +28 -0
  24. package/apps/shared/components/admin/diagnostics/format.ts +106 -0
  25. package/apps/shared/components/admin/diagnostics/logClient.ts +231 -0
  26. package/apps/shared/components/admin/diagnostics/logLines.ts +125 -0
  27. package/apps/shared/components/admin/diagnostics/metricsHistory.ts +28 -0
  28. package/apps/shared/components/admin/diagnostics/useDiagnosticsResource.ts +44 -0
  29. package/apps/shared/components/admin/diagnostics/useLogStream.ts +122 -0
  30. package/apps/shared/components/admin/diagnostics/useMetricsPolling.ts +84 -0
  31. package/apps/shared/components/admin/layout/AdminShell.tsx +3 -0
  32. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -2
  34. package/apps/shared/components/mail/layout/MailShell.tsx +1 -2
  35. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -2
  36. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -2
  37. package/apps/shared/navigation/index.ts +29 -2
  38. package/dist/apps/admin/diagnostics/index.d.ts +3 -0
  39. package/dist/apps/admin/diagnostics/index.js +6 -0
  40. package/dist/apps/shared/components/admin/diagnostics/Badge.d.ts +10 -0
  41. package/dist/apps/shared/components/admin/diagnostics/Badge.js +15 -0
  42. package/dist/apps/shared/components/admin/diagnostics/ComponentsTable.d.ts +15 -0
  43. package/dist/apps/shared/components/admin/diagnostics/ComponentsTable.js +35 -0
  44. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +17 -0
  45. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +111 -0
  46. package/dist/apps/shared/components/admin/diagnostics/HostCard.d.ts +13 -0
  47. package/dist/apps/shared/components/admin/diagnostics/HostCard.js +33 -0
  48. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.d.ts +26 -0
  49. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +60 -0
  50. package/dist/apps/shared/components/admin/diagnostics/KubernetesNotice.d.ts +8 -0
  51. package/dist/apps/shared/components/admin/diagnostics/KubernetesNotice.js +9 -0
  52. package/dist/apps/shared/components/admin/diagnostics/LogStore.d.ts +37 -0
  53. package/dist/apps/shared/components/admin/diagnostics/LogStore.js +50 -0
  54. package/dist/apps/shared/components/admin/diagnostics/LogViewer.d.ts +24 -0
  55. package/dist/apps/shared/components/admin/diagnostics/LogViewer.js +47 -0
  56. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.d.ts +13 -0
  57. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +63 -0
  58. package/dist/apps/shared/components/admin/diagnostics/MetricTile.d.ts +21 -0
  59. package/dist/apps/shared/components/admin/diagnostics/MetricTile.js +7 -0
  60. package/dist/apps/shared/components/admin/diagnostics/NamespaceCard.d.ts +13 -0
  61. package/dist/apps/shared/components/admin/diagnostics/NamespaceCard.js +15 -0
  62. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.d.ts +8 -0
  63. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.js +31 -0
  64. package/dist/apps/shared/components/admin/diagnostics/ProcessCard.d.ts +8 -0
  65. package/dist/apps/shared/components/admin/diagnostics/ProcessCard.js +18 -0
  66. package/dist/apps/shared/components/admin/diagnostics/PvcTable.d.ts +6 -0
  67. package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +18 -0
  68. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.d.ts +7 -0
  69. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +28 -0
  70. package/dist/apps/shared/components/admin/diagnostics/ServerVersionCard.d.ts +6 -0
  71. package/dist/apps/shared/components/admin/diagnostics/ServerVersionCard.js +17 -0
  72. package/dist/apps/shared/components/admin/diagnostics/Sparkline.d.ts +22 -0
  73. package/dist/apps/shared/components/admin/diagnostics/Sparkline.js +34 -0
  74. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.d.ts +6 -0
  75. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +26 -0
  76. package/dist/apps/shared/components/admin/diagnostics/UsageMeter.d.ts +26 -0
  77. package/dist/apps/shared/components/admin/diagnostics/UsageMeter.js +25 -0
  78. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +10 -0
  79. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +11 -0
  80. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +168 -0
  81. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +17 -0
  82. package/dist/apps/shared/components/admin/diagnostics/download.d.ts +9 -0
  83. package/dist/apps/shared/components/admin/diagnostics/download.js +23 -0
  84. package/dist/apps/shared/components/admin/diagnostics/format.d.ts +24 -0
  85. package/dist/apps/shared/components/admin/diagnostics/format.js +91 -0
  86. package/dist/apps/shared/components/admin/diagnostics/logClient.d.ts +85 -0
  87. package/dist/apps/shared/components/admin/diagnostics/logClient.js +173 -0
  88. package/dist/apps/shared/components/admin/diagnostics/logLines.d.ts +47 -0
  89. package/dist/apps/shared/components/admin/diagnostics/logLines.js +86 -0
  90. package/dist/apps/shared/components/admin/diagnostics/metricsHistory.d.ts +9 -0
  91. package/dist/apps/shared/components/admin/diagnostics/metricsHistory.js +19 -0
  92. package/dist/apps/shared/components/admin/diagnostics/useDiagnosticsResource.d.ts +12 -0
  93. package/dist/apps/shared/components/admin/diagnostics/useDiagnosticsResource.js +30 -0
  94. package/dist/apps/shared/components/admin/diagnostics/useLogStream.d.ts +37 -0
  95. package/dist/apps/shared/components/admin/diagnostics/useLogStream.js +79 -0
  96. package/dist/apps/shared/components/admin/diagnostics/useMetricsPolling.d.ts +20 -0
  97. package/dist/apps/shared/components/admin/diagnostics/useMetricsPolling.js +69 -0
  98. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  99. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  100. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  101. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -2
  102. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -2
  103. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -2
  104. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -2
  105. package/dist/apps/shared/navigation/index.d.ts +11 -0
  106. package/dist/apps/shared/navigation/index.js +26 -2
  107. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { LogClient } from "./logClient.js";
7
+ import { LogStore } from "./LogStore.js";
8
+ /** Lines arrive in bursts; the view is redrawn at most this often, however many came. */
9
+ export const LOG_FLUSH_MS = 100;
10
+ function snapshotOf(store) {
11
+ const capture = store.capture;
12
+ return {
13
+ entries: store.entries.slice(),
14
+ dropped: store.dropped,
15
+ capture: capture && {
16
+ active: capture.active,
17
+ startedAt: capture.startedAt,
18
+ stoppedAt: capture.stoppedAt,
19
+ count: capture.entries.length,
20
+ missed: capture.missed,
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * The live log stream of the server for the log view: a `LogClient`, and a `LogStore` holding the lines and any capture. Nothing
26
+ * connects until `start()`. The stream is closed, and the redraw timer cleared, when the component unmounts.
27
+ */
28
+ export function useLogStream(options = {}) {
29
+ const [client] = useState(() => new LogClient(options));
30
+ const [store] = useState(() => new LogStore());
31
+ const [connection, setConnection] = useState({
32
+ status: client.status,
33
+ error: undefined,
34
+ });
35
+ const [snapshot, setSnapshot] = useState(() => snapshotOf(store));
36
+ const timer = useRef(undefined);
37
+ const flush = useCallback(() => {
38
+ clearTimeout(timer.current);
39
+ timer.current = undefined;
40
+ setSnapshot(snapshotOf(store));
41
+ }, [store]);
42
+ useEffect(() => {
43
+ const stopEntries = client.onEntry((entry) => {
44
+ store.add(entry);
45
+ timer.current ?? (timer.current = setTimeout(flush, LOG_FLUSH_MS));
46
+ });
47
+ const stopStatus = client.onStatus((status, error) => setConnection({ status, error }));
48
+ return () => {
49
+ stopEntries();
50
+ stopStatus();
51
+ client.stop();
52
+ clearTimeout(timer.current);
53
+ timer.current = undefined;
54
+ };
55
+ }, [client, store, flush]);
56
+ return {
57
+ status: connection.status,
58
+ error: connection.error,
59
+ running: connection.status === "connecting" || connection.status === "live" || connection.status === "reconnecting",
60
+ entries: snapshot.entries,
61
+ dropped: snapshot.dropped,
62
+ capture: snapshot.capture,
63
+ start: () => client.start(),
64
+ stop: () => client.stop(),
65
+ clear: () => {
66
+ store.clear();
67
+ flush();
68
+ },
69
+ startCapture: () => {
70
+ store.startCapture(Date.now());
71
+ flush();
72
+ },
73
+ stopCapture: () => {
74
+ store.stopCapture(Date.now());
75
+ flush();
76
+ },
77
+ captureEntries: () => store.capture?.entries ?? [],
78
+ };
79
+ }
@@ -0,0 +1,20 @@
1
+ import { DiagnosticsMetrics } from "./diagnosticsApi.js";
2
+ export interface MetricsPolling {
3
+ /** The last `METRICS_HISTORY_SIZE` samples, oldest first. */
4
+ history: DiagnosticsMetrics[];
5
+ /** Why the last request failed, until one succeeds. */
6
+ error: string | undefined;
7
+ /** Whether nothing has been answered yet. */
8
+ loading: boolean;
9
+ /** Whether polling is stopped by the viewer (Pause), or because the server refused the viewer. */
10
+ paused: boolean;
11
+ setPaused: (paused: boolean) => void;
12
+ }
13
+ /**
14
+ * Samples `getDiagnosticsMetrics()` every `METRICS_POLL_INTERVAL_MS` while `enabled` and not paused, keeping the last
15
+ * `METRICS_HISTORY_SIZE`. A request is only ever made when the previous one has answered, none is made while the page is
16
+ * hidden (one is made at once when it is shown again), and polling stops when the component unmounts. A refusal (401/403)
17
+ * pauses polling, since asking again would only be refused again; Resume tries once more. Any other failure is shown
18
+ * and polling carries on, so a blip on the server does not need the viewer to do anything.
19
+ */
20
+ export declare function useMetricsPolling(enabled: boolean): MetricsPolling;
@@ -0,0 +1,69 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useState } from "react";
6
+ import { getDiagnosticsMetrics } from "./diagnosticsApi.js";
7
+ import { describeError, isPermissionError } from "./format.js";
8
+ import { appendSample, METRICS_POLL_INTERVAL_MS } from "./metricsHistory.js";
9
+ /**
10
+ * Samples `getDiagnosticsMetrics()` every `METRICS_POLL_INTERVAL_MS` while `enabled` and not paused, keeping the last
11
+ * `METRICS_HISTORY_SIZE`. A request is only ever made when the previous one has answered, none is made while the page is
12
+ * hidden (one is made at once when it is shown again), and polling stops when the component unmounts. A refusal (401/403)
13
+ * pauses polling, since asking again would only be refused again; Resume tries once more. Any other failure is shown
14
+ * and polling carries on, so a blip on the server does not need the viewer to do anything.
15
+ */
16
+ export function useMetricsPolling(enabled) {
17
+ const [history, setHistory] = useState([]);
18
+ const [error, setError] = useState();
19
+ const [loading, setLoading] = useState(true);
20
+ const [paused, setPaused] = useState(false);
21
+ useEffect(() => {
22
+ if (!enabled || paused) {
23
+ return;
24
+ }
25
+ let cancelled = false;
26
+ let inFlight = false;
27
+ async function sample() {
28
+ if (inFlight || document.hidden) {
29
+ return;
30
+ }
31
+ inFlight = true;
32
+ try {
33
+ const next = await getDiagnosticsMetrics();
34
+ if (!cancelled) {
35
+ setHistory((previous) => appendSample(previous, next));
36
+ setError(undefined);
37
+ }
38
+ }
39
+ catch (err) {
40
+ if (!cancelled) {
41
+ setError(describeError(err, "Could not read the server's metrics."));
42
+ if (isPermissionError(err)) {
43
+ setPaused(true);
44
+ }
45
+ }
46
+ }
47
+ finally {
48
+ inFlight = false;
49
+ if (!cancelled) {
50
+ setLoading(false);
51
+ }
52
+ }
53
+ }
54
+ void sample();
55
+ const timer = setInterval(() => void sample(), METRICS_POLL_INTERVAL_MS);
56
+ const onVisibilityChange = () => {
57
+ if (!document.hidden) {
58
+ void sample();
59
+ }
60
+ };
61
+ document.addEventListener("visibilitychange", onVisibilityChange);
62
+ return () => {
63
+ cancelled = true;
64
+ clearInterval(timer);
65
+ document.removeEventListener("visibilitychange", onVisibilityChange);
66
+ };
67
+ }, [enabled, paused]);
68
+ return { history, error, loading, paused, setPaused };
69
+ }
@@ -2,7 +2,7 @@ import "../../../styles/app.css";
2
2
  import React, { PropsWithChildren } from "react";
3
3
  import type { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
4
4
  import { PluginNav, PluginNavProps } from "../../../plugins/pluginNav.js";
5
- export type AdminSection = "mailboxes" | "quarantine" | "ingestQueue" | "domains" | "auditLog" | "distributionLists" | "transportRules" | "escrowScopes" | "retentionPolicy" | "encryptionPolicy" | "mailboxPolicy" | "setup" | "dataRequests" | "plugins" | "branding" | "signingCertificates";
5
+ export type AdminSection = "mailboxes" | "quarantine" | "ingestQueue" | "domains" | "auditLog" | "distributionLists" | "transportRules" | "escrowScopes" | "retentionPolicy" | "encryptionPolicy" | "mailboxPolicy" | "setup" | "dataRequests" | "plugins" | "diagnostics" | "branding" | "signingCertificates";
6
6
  /** A core `AdminSection`, or a plugin's `adminNav` item id (see `PluginNav`). */
7
7
  export type AdminShellActive = AdminSection | (string & {});
8
8
  export interface AdminShellProps extends PluginNavProps {
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import "../../../styles/app.css";
7
7
  import { useEffect, useState } from "react";
8
8
  import { useRouter } from "@rapidrest/react/client";
9
- import { HiOutlineClipboardDocumentList, HiOutlineClock, HiOutlineDocumentArrowDown, HiOutlineDocumentCheck, HiOutlineGlobeAlt, HiOutlineInboxStack, HiOutlineKey, HiOutlineLockClosed, HiOutlinePaintBrush, HiOutlinePuzzlePiece, HiOutlineQueueList, HiOutlineRocketLaunch, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineUserGroup, HiOutlineWrenchScrewdriver, HiOutlineBars3, } from "react-icons/hi2";
9
+ import { HiOutlineClipboardDocumentList, HiOutlineClock, HiOutlineCpuChip, HiOutlineDocumentArrowDown, HiOutlineDocumentCheck, HiOutlineGlobeAlt, HiOutlineInboxStack, HiOutlineKey, HiOutlineLockClosed, HiOutlinePaintBrush, HiOutlinePuzzlePiece, HiOutlineQueueList, HiOutlineRocketLaunch, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineUserGroup, HiOutlineWrenchScrewdriver, HiOutlineBars3, } from "react-icons/hi2";
10
10
  import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
11
11
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
12
12
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
@@ -78,6 +78,7 @@ const NAV_ITEMS = [
78
78
  icon: HiOutlineDocumentCheck,
79
79
  },
80
80
  { id: "plugins", href: "/admin/plugins", label: "Plugins", icon: HiOutlinePuzzlePiece },
81
+ { id: "diagnostics", href: "/admin/diagnostics", label: "Diagnostics", icon: HiOutlineCpuChip },
81
82
  { id: "branding", href: "/admin/branding", label: "Branding", icon: HiOutlinePaintBrush },
82
83
  ];
83
84
  /** Mailbox-scoped sections — not part of the global nav rail/tab bar (see `NAV_ITEMS` above), but
@@ -10,7 +10,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
12
  import AppShell from "../../layout/AppShell.js";
13
- import { useLocation } from "@rapidrest/react/client";
13
+ import { useLocation } from "../../../navigation/index.js";
14
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
15
  import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
16
  const CalendarShellContext = createContext({
@@ -11,8 +11,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocation } from "@rapidrest/react/client";
15
- import { useNavigate } from "../../../navigation/index.js";
14
+ import { useLocation, useNavigate } from "../../../navigation/index.js";
16
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
16
  import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
18
17
  const ContactsShellContext = createContext({ mailboxes: [] });
@@ -10,8 +10,7 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
11
  import AppShell from "../../layout/AppShell.js";
12
12
  import FloatingActionButton from "../../layout/FloatingActionButton.js";
13
- import { useLocation } from "@rapidrest/react/client";
14
- import { useNavigate } from "../../../navigation/index.js";
13
+ import { useLocation, useNavigate } from "../../../navigation/index.js";
15
14
  import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
16
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
16
  import { prefetchComposeWindow, useCompose } from "../compose/ComposeContext.js";
@@ -11,8 +11,7 @@ import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocation } from "@rapidrest/react/client";
15
- import { useNavigate } from "../../../navigation/index.js";
14
+ import { useLocation, useNavigate } from "../../../navigation/index.js";
16
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
16
  import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
18
17
  import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
@@ -11,8 +11,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocation } from "@rapidrest/react/client";
15
- import { useNavigate } from "../../../navigation/index.js";
14
+ import { useLocation, useNavigate } from "../../../navigation/index.js";
16
15
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
17
16
  import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
18
17
  const TasksShellContext = createContext({ mailboxes: [] });
@@ -14,3 +14,14 @@ export type NavigateFn = (href: string, options?: NavigateOptions) => void;
14
14
  * stable across renders.
15
15
  */
16
16
  export declare function useNavigate(): NavigateFn;
17
+ /**
18
+ * Where the page is, as `{ pathname, search, hash }`: the router's location when the page is under a router, else the browser's.
19
+ * A page a plugin renders has no router (`router = false`), where `@rapidrest/react`'s `useLocation()` is empty and the query a
20
+ * shell reads its mailbox from (`?mailboxUid=`) would be lost. Without one, the address is the browser's - and empty on the
21
+ * server and while hydrating, so the page hydrates as it was rendered and then follows the address.
22
+ */
23
+ export declare function useLocation(): {
24
+ pathname: string;
25
+ search: string;
26
+ hash: string;
27
+ };
@@ -7,8 +7,8 @@
7
7
  * route, the app's `_shell.tsx` as the persistent frame - see `apps/www/_shell.tsx`); this is the one thing on top of it: a
8
8
  * `navigate()` that keeps the page when only the query string changes.
9
9
  */
10
- import { useCallback, useRef } from "react";
11
- import { useRouter } from "@rapidrest/react/client";
10
+ import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
11
+ import { useLocation as useRouterLocation, useRouter } from "@rapidrest/react/client";
12
12
  /**
13
13
  * The function to go somewhere with, for what code decides (after a save, a select, a folder change). `navigate("/calendar")`
14
14
  * changes the page and the URL without a page load when the router knows the page (Mail, Calendar, Contacts, Tasks, Settings
@@ -32,3 +32,27 @@ export function useNavigate() {
32
32
  void go(href, { replace: options.replace, shallow });
33
33
  }, []);
34
34
  }
35
+ function subscribeToHistory(onChange) {
36
+ window.addEventListener("popstate", onChange);
37
+ return () => window.removeEventListener("popstate", onChange);
38
+ }
39
+ // Joined, not added: a test's stand-in for `window.location` may have no `search`, and `undefined + undefined` is `NaN`.
40
+ const currentAddress = () => [window.location.pathname, window.location.search, window.location.hash].join("");
41
+ const noAddress = () => "";
42
+ /**
43
+ * Where the page is, as `{ pathname, search, hash }`: the router's location when the page is under a router, else the browser's.
44
+ * A page a plugin renders has no router (`router = false`), where `@rapidrest/react`'s `useLocation()` is empty and the query a
45
+ * shell reads its mailbox from (`?mailboxUid=`) would be lost. Without one, the address is the browser's - and empty on the
46
+ * server and while hydrating, so the page hydrates as it was rendered and then follows the address.
47
+ */
48
+ export function useLocation() {
49
+ const routed = useRouterLocation();
50
+ const own = useSyncExternalStore(subscribeToHistory, currentAddress, noAddress);
51
+ const ownLocation = useMemo(() => {
52
+ // Split by hand, not with `URL`: a page that stubs the global `URL` (for `createObjectURL`) must still render.
53
+ const [beforeHash, ...fragment] = own.split("#");
54
+ const [pathname, ...query] = beforeHash.split("?");
55
+ return { pathname, search: query.length ? `?${query.join("?")}` : "", hash: fragment.length ? `#${fragment.join("#")}` : "" };
56
+ }, [own]);
57
+ return routed.pathname !== "" ? routed : ownLocation;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/web-client",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "RapidMX's webmail and admin console React UI for web-based clients.",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/rapidmx/web-client",