(null);
+ const [width, setWidth] = useState(220);
+
+ useEffect(() => {
+ const node = containerRef.current;
+ if (!node) {
+ return;
+ }
+
+ const observer = new ResizeObserver((entries) => {
+ const nextWidth = entries[0]?.contentRect.width;
+ if (nextWidth && nextWidth > 0) {
+ setWidth(nextWidth);
+ }
+ });
+ observer.observe(node);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, []);
+
+ if (trend.points.length < 2) {
+ return null;
+ }
+
+ const chartData: SparklineDatum[] = trend.points.map((point) => ({
+ name: title,
+ x: point.label,
+ y: point.value,
+ }));
+
+ const formatTooltip = (datum: SparklineDatum) =>
+ intl.formatMessage(messages.trendTooltip, {
+ date: datum.x,
+ metric: title,
+ value: datum.y,
+ });
+
+ return (
+
+ formatTooltip(datum as SparklineDatum)}
+ />
+ }
+ height={52}
+ padding={{ bottom: 2, left: 2, right: 2, top: 2 }}
+ themeColor={ChartThemeColor.blue}
+ width={width}
+ >
+
+
+
+ );
+}
diff --git a/packages/operational-dashboard-ui/src/dashboard/utilization-chart.tsx b/packages/operational-dashboard-ui/src/dashboard/utilization-chart.tsx
new file mode 100644
index 00000000..1d9b499b
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/dashboard/utilization-chart.tsx
@@ -0,0 +1,77 @@
+import { ChartDonutUtilization } from "@patternfly/react-charts/victory";
+import { useIntl } from "react-intl";
+
+import type {
+ OperationalMetric,
+ OperationalMetricTrend,
+} from "../application/dashboard-types";
+import { messages } from "../messages";
+import "../pages/dashboard-widget.css";
+
+interface UsageData {
+ x: string;
+ y: number;
+}
+
+interface UtilizationMetric {
+ id: string;
+ total: string;
+ trend?: OperationalMetricTrend;
+ unit: string;
+ value: string;
+}
+
+export function isUtilizationMetric(
+ metric: OperationalMetric,
+): metric is UtilizationMetric {
+ return typeof metric.unit === "string" && typeof metric.total === "string";
+}
+
+export function UtilizationChart({
+ metric,
+}: Readonly<{ metric: OperationalMetric }>) {
+ const intl = useIntl();
+
+ if (!isUtilizationMetric(metric)) {
+ return null;
+ }
+
+ const { unit, total, value } = metric;
+ const percentage = Math.round((Number(value) / Number(total)) * 100);
+ const capacityLabel = intl.formatMessage(messages.utilizationCapacity, {
+ unit,
+ });
+
+ const data: UsageData = { x: capacityLabel, y: percentage };
+
+ return (
+
+
+ datum.x
+ ? intl.formatMessage(messages.utilizationDataLabel, {
+ capacity: datum.x,
+ percentage: datum.y,
+ })
+ : null
+ }
+ name="chart1"
+ subTitle={intl.formatMessage(messages.utilizationSubtitle, {
+ total,
+ unit,
+ })}
+ title={intl.formatMessage(messages.utilizationLabel, {
+ unit,
+ value,
+ })}
+ thresholds={[{ value: 60 }, { value: 90 }]}
+ />
+
+ );
+}
diff --git a/packages/operational-dashboard-ui/src/fixtures/mock-operational-dashboard-metrics.ts b/packages/operational-dashboard-ui/src/fixtures/mock-operational-dashboard-metrics.ts
new file mode 100644
index 00000000..1f2454b8
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/fixtures/mock-operational-dashboard-metrics.ts
@@ -0,0 +1,101 @@
+import type { OperationalDashboardMetrics } from "../application/dashboard-types";
+
+export const mockOperationalDashboardMetrics: OperationalDashboardMetrics =
+ Object.freeze({
+ metrics: Object.freeze([
+ Object.freeze({
+ id: "active-users",
+ trend: Object.freeze({
+ points: Object.freeze([
+ Object.freeze({ label: "Day 1", value: 521 }),
+ Object.freeze({ label: "Day 2", value: 544 }),
+ Object.freeze({ label: "Day 3", value: 553 }),
+ Object.freeze({ label: "Day 4", value: 578 }),
+ Object.freeze({ label: "Day 5", value: 590 }),
+ Object.freeze({ label: "Day 6", value: 601 }),
+ Object.freeze({ label: "Day 7", value: 612 }),
+ ]),
+ }),
+ value: "612",
+ }),
+ Object.freeze({
+ id: "provisioned-gateways",
+ trend: Object.freeze({
+ points: Object.freeze([
+ Object.freeze({ label: "Day 1", value: 82 }),
+ Object.freeze({ label: "Day 2", value: 84 }),
+ Object.freeze({ label: "Day 3", value: 85 }),
+ Object.freeze({ label: "Day 4", value: 89 }),
+ Object.freeze({ label: "Day 5", value: 90 }),
+ Object.freeze({ label: "Day 6", value: 93 }),
+ Object.freeze({ label: "Day 7", value: 97 }),
+ ]),
+ }),
+ value: "97",
+ }),
+ Object.freeze({
+ id: "provisioned-sandboxes",
+ trend: Object.freeze({
+ points: Object.freeze([
+ Object.freeze({ label: "Day 1", value: 176 }),
+ Object.freeze({ label: "Day 2", value: 182 }),
+ Object.freeze({ label: "Day 3", value: 189 }),
+ Object.freeze({ label: "Day 4", value: 194 }),
+ Object.freeze({ label: "Day 5", value: 201 }),
+ Object.freeze({ label: "Day 6", value: 207 }),
+ Object.freeze({ label: "Day 7", value: 214 }),
+ ]),
+ }),
+ value: "214",
+ }),
+ Object.freeze({
+ id: "namespaces",
+ trend: Object.freeze({
+ points: Object.freeze([
+ Object.freeze({ label: "Day 1", value: 12 }),
+ Object.freeze({ label: "Day 2", value: 18 }),
+ Object.freeze({ label: "Day 3", value: 20 }),
+ Object.freeze({ label: "Day 4", value: 27 }),
+ Object.freeze({ label: "Day 5", value: 55 }),
+ Object.freeze({ label: "Day 6", value: 74 }),
+ Object.freeze({ label: "Day 7", value: 80 }),
+ ]),
+ }),
+ value: "80",
+ }),
+ Object.freeze({
+ id: "nodes",
+ value: "8",
+ }),
+ Object.freeze({
+ id: "cpu",
+ value: "4.2",
+ unit: "cores",
+ total: "60",
+ }),
+ Object.freeze({
+ id: "memory",
+ value: "92.3",
+ unit: "GiB",
+ total: "237",
+ }),
+ Object.freeze({
+ id: "pods",
+ value: "548",
+ total: "2000",
+ unit: "pods",
+ trend: Object.freeze({
+ points: Object.freeze([
+ Object.freeze({ label: "Day 1", value: 124 }),
+ Object.freeze({ label: "Day 2", value: 182 }),
+ Object.freeze({ label: "Day 3", value: 301 }),
+ Object.freeze({ label: "Day 4", value: 361 }),
+ Object.freeze({ label: "Day 5", value: 449 }),
+ Object.freeze({ label: "Day 6", value: 525 }),
+ Object.freeze({ label: "Day 7", value: 538 }),
+ ]),
+ }),
+ }),
+ ]),
+ lastSuccessfulRefresh: new Date("2026-08-25T10:55:00.000Z"),
+ });
diff --git a/packages/operational-dashboard-ui/src/index.ts b/packages/operational-dashboard-ui/src/index.ts
new file mode 100644
index 00000000..8b96b161
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/index.ts
@@ -0,0 +1,37 @@
+export {
+ DashboardUiProvider,
+ useDashboardUi,
+ type DashboardUiNavigation,
+} from "./dashboard-ui-provider";
+export type {
+ DashboardControlPlane,
+ DashboardInvocationContext,
+ DashboardOperations,
+ DashboardProbe,
+ DashboardProbeAction,
+ DashboardProbeName,
+ DashboardProbeOutcome,
+ DashboardProbePublisher,
+ DashboardWorkflowAction,
+ DashboardWorkflowRuntime,
+ OperationalDashboardMetrics,
+ OperationalMetric,
+ OperationalMetricTrend,
+ OperationalMetricTrendPoint,
+ SignupTrendPoint,
+} from "./application/dashboard-types";
+export { noopDashboardProbePublisher } from "./application/dashboard-probes";
+export {
+ createDashboardOperations,
+ type DashboardOperationDependencies,
+} from "./application/dashboard-operations";
+export {
+ operationalDashboardMetricsQueryKey,
+ operationalDashboardRefreshMilliseconds,
+} from "./dashboard/dashboard-data";
+export { ResourceRefreshButton } from "./shared/resource-refresh-button";
+export {
+ OperationalDashboardPage,
+ type OperationalDashboardPageProps,
+} from "./pages/operational-dashboard-page";
+export { messages as dashboardMessages } from "./messages";
diff --git a/packages/operational-dashboard-ui/src/messages.ts b/packages/operational-dashboard-ui/src/messages.ts
new file mode 100644
index 00000000..da54e7ac
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/messages.ts
@@ -0,0 +1,185 @@
+import { defineMessages } from "react-intl";
+
+export const messages = defineMessages({
+ activeUsers: {
+ id: "app.dashboard.widget.activeUsers",
+ defaultMessage: "Active users",
+ description: "Title for the active users dashboard widget.",
+ },
+ cpus: {
+ id: "app.dashboard.summary.cpus",
+ defaultMessage: "CPUs",
+ description: "Summary label for provisioned CPU capacity.",
+ },
+ description: {
+ id: "app.dashboard.description",
+ defaultMessage:
+ "This dashboard is in development and is currently using mocked data.",
+ description: "Supporting text on the operational dashboard page.",
+ },
+ gateways: {
+ id: "app.dashboard.summary.gateways",
+ defaultMessage: "Gateways",
+ description: "Summary label for provisioned gateways.",
+ },
+ loadErrorBody: {
+ id: "app.dashboard.loadError.body",
+ defaultMessage:
+ "An unexpected error occurred while loading dashboard metrics.",
+ description:
+ "Recovery guidance when operational dashboard metrics cannot be loaded.",
+ },
+ loadErrorTitle: {
+ id: "app.dashboard.loadError.title",
+ defaultMessage: "Operational dashboard metrics are unavailable",
+ description:
+ "Title shown when operational dashboard metrics cannot be loaded.",
+ },
+ loading: {
+ id: "app.dashboard.loading",
+ defaultMessage: "Loading operational dashboard metrics",
+ description:
+ "Accessible status shown while operational dashboard metrics load.",
+ },
+ memory: {
+ id: "app.dashboard.summary.memory",
+ defaultMessage: "Memory",
+ description: "Summary label for memory utilization.",
+ },
+ metricUnavailableBody: {
+ id: "app.dashboard.metricUnavailable.body",
+ defaultMessage: "This information is not currently available.",
+ description:
+ "Recovery guidance when an individual dashboard metric is missing.",
+ },
+ metricUnavailableTitle: {
+ id: "app.dashboard.metricUnavailable.title",
+ defaultMessage: "Metric unavailable",
+ description:
+ "Heading shown when an individual dashboard metric is missing.",
+ },
+ metricValue: {
+ id: "app.dashboard.metric.value",
+ defaultMessage: "{value} {label}",
+ description: "Formatted count for a dashboard metric card heading.",
+ },
+ namespaces: {
+ id: "app.dashboard.widget.namespaces",
+ defaultMessage: "Namespaces",
+ description: "Title for the namespaces dashboard widget.",
+ },
+ nodes: {
+ id: "app.dashboard.widget.nodes",
+ defaultMessage: "Nodes",
+ description: "Title for the nodes dashboard widget.",
+ },
+ pods: {
+ id: "app.dashboard.summary.pods",
+ defaultMessage: "Pods",
+ description: "Summary label for pod utilization.",
+ },
+ provisionedGateways: {
+ id: "app.dashboard.widget.provisionedGateways",
+ defaultMessage: "Provisioned gateways",
+ description: "Title for the provisioned gateways dashboard widget.",
+ },
+ provisionedSandboxes: {
+ id: "app.dashboard.widget.provisionedSandboxes",
+ defaultMessage: "Provisioned sandboxes",
+ description: "Title for the provisioned sandboxes dashboard widget.",
+ },
+ refresh: {
+ id: "app.dashboard.refresh",
+ defaultMessage: "Refresh dashboard metrics",
+ description:
+ "Accessible label for refreshing operational dashboard metrics.",
+ },
+ summary: {
+ id: "app.dashboard.widget.summary",
+ defaultMessage: "Summary",
+ description: "Title for the operational dashboard summary widget.",
+ },
+ summarySystem: {
+ id: "app.dashboard.summary.system",
+ defaultMessage: "System",
+ description:
+ "Heading for system utilization metrics in the summary widget.",
+ },
+ summarySystemAriaLabel: {
+ id: "app.dashboard.summary.systemAriaLabel",
+ defaultMessage: "System metrics",
+ description:
+ "Accessible label for the system metrics list in the summary widget.",
+ },
+ summaryUsage: {
+ id: "app.dashboard.summary.usage",
+ defaultMessage: "Usage",
+ description: "Heading for adoption metrics in the summary widget.",
+ },
+ summaryUsageAriaLabel: {
+ id: "app.dashboard.summary.usageAriaLabel",
+ defaultMessage: "Usage metrics",
+ description:
+ "Accessible label for the usage metrics list in the summary widget.",
+ },
+ title: {
+ id: "app.dashboard.title",
+ defaultMessage: "HyperShell operational dashboard",
+ description: "Main heading on the operational dashboard page.",
+ },
+ trendTooltip: {
+ id: "app.dashboard.trend.tooltip",
+ defaultMessage: "{date}: {value} {metric}",
+ description: "Tooltip for a dashboard metric trend sparkline point.",
+ },
+ users: {
+ id: "app.dashboard.summary.users",
+ defaultMessage: "Users",
+ description: "Summary label for active users.",
+ },
+ utilizationCapacity: {
+ id: "app.dashboard.utilization.capacity",
+ defaultMessage: "{unit} capacity",
+ description: "Capacity label for a utilization donut chart.",
+ },
+ utilizationChartTitle: {
+ id: "app.dashboard.utilization.chartTitle",
+ defaultMessage: "{unit} utilization chart",
+ description: "Accessible title for a utilization donut chart.",
+ },
+ utilizationDataLabel: {
+ id: "app.dashboard.utilization.dataLabel",
+ defaultMessage: "{capacity}: {percentage}%",
+ description: "Data label for a utilization donut chart segment.",
+ },
+ utilizationLabel: {
+ id: "app.dashboard.utilization.label",
+ defaultMessage: "{value} {unit}",
+ description: "Primary value label for a utilization donut chart.",
+ },
+ utilizationSubtitle: {
+ id: "app.dashboard.utilization.subtitle",
+ defaultMessage: "of {total} {unit}",
+ description: "Subtitle for a utilization donut chart.",
+ },
+ widgetCpu: {
+ id: "app.dashboard.widget.cpu",
+ defaultMessage: "CPU",
+ description: "Title for the CPU utilization dashboard widget.",
+ },
+ widgetMemory: {
+ id: "app.dashboard.widget.memory",
+ defaultMessage: "Memory",
+ description: "Title for the memory utilization dashboard widget.",
+ },
+ widgetPods: {
+ id: "app.dashboard.widget.pods",
+ defaultMessage: "Pods",
+ description: "Title for the pods utilization dashboard widget.",
+ },
+ widgetSandboxes: {
+ id: "app.dashboard.summary.sandboxes",
+ defaultMessage: "Sandboxes",
+ description: "Summary label for provisioned sandboxes.",
+ },
+});
diff --git a/packages/operational-dashboard-ui/src/pages/dashboard-widget.css b/packages/operational-dashboard-ui/src/pages/dashboard-widget.css
new file mode 100644
index 00000000..c6380ee3
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/pages/dashboard-widget.css
@@ -0,0 +1,26 @@
+/*
+ * Widget action menus render in a document-level popper. Hide Remove until product
+ * supports an add-widgets drawer to restore removed tiles.
+ */
+body.hypershell-operational-dashboard [data-ouia-component-id="remove-widget"] {
+ display: none;
+}
+
+.hypershell-dashboard-metric-card {
+ --pf-v6-c-content--MarginBottom: 0;
+}
+
+.hypershell-dashboard-metric-card .pf-v6-c-stack {
+ --pf-v6-c-stack--m-gutter--Gap: var(--pf-t--global--spacer--sm);
+}
+
+.hypershell-dashboard-sparkline-chart {
+ height: 52px;
+ width: 100%;
+}
+
+.hypershell-dashboard-utilization-chart {
+ aspect-ratio: 1 / 1;
+ height: 100%;
+ margin-inline: auto;
+}
diff --git a/packages/operational-dashboard-ui/src/pages/dashboard-widget.tsx b/packages/operational-dashboard-ui/src/pages/dashboard-widget.tsx
new file mode 100644
index 00000000..5e56f7d8
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/pages/dashboard-widget.tsx
@@ -0,0 +1,197 @@
+import {
+ Card,
+ CardBody,
+ Content,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Flex,
+ FlexItem,
+ Stack,
+ StackItem,
+ Title,
+} from "@patternfly/react-core";
+import type { PropsWithChildren } from "react";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import type { OperationalMetric } from "../application/dashboard-types";
+import { TrendSparklineChart } from "../dashboard/trend-sparkline-chart";
+import {
+ isUtilizationMetric,
+ UtilizationChart,
+} from "../dashboard/utilization-chart";
+import { messages } from "../messages";
+
+function WidgetContent({ children }: Readonly) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function MetricCard({
+ metric,
+ subtitle,
+ title,
+}: Readonly<{ metric: OperationalMetric; subtitle: string; title: string }>) {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+
+
+
+ {intl.formatMessage(messages.metricValue, {
+ label: title,
+ value: metric.value,
+ })}
+
+ {subtitle ? {subtitle} : null}
+
+
+
+ {metric.trend ? (
+
+
+
+ ) : null}
+
+
+
+ );
+}
+
+export function UtilizationCard({
+ metric,
+ subtitle,
+}: Readonly<{ metric: OperationalMetric; subtitle: string; title: string }>) {
+ return (
+
+
+
+
+
+
+
+ {metric.value} {metric.unit}
+
+ {subtitle}
+
+
+
+ {isUtilizationMetric(metric) ? (
+
+
+
+ ) : null}
+
+
+
+ );
+}
+
+export function SummaryCard({
+ metrics,
+}: Readonly<{ metrics: readonly OperationalMetric[] }>) {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {metrics.find((metric) => metric.id === "active-users")?.value}
+
+
+
+
+
+
+
+ {
+ metrics.find((metric) => metric.id === "provisioned-gateways")
+ ?.value
+ }
+
+
+
+
+
+
+
+ {metrics.find((metric) => metric.id === "namespaces")?.value}
+
+
+
+
+
+
+
+ {
+ metrics.find(
+ (metric) => metric.id === "provisioned-sandboxes",
+ )?.value
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {metrics.find((metric) => metric.id === "memory")?.value}
+
+
+
+
+
+
+
+ {metrics.find((metric) => metric.id === "cpu")?.value}
+
+
+
+
+
+
+
+ {metrics.find((metric) => metric.id === "pods")?.value}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/operational-dashboard-ui/src/pages/get-metrics-data.ts b/packages/operational-dashboard-ui/src/pages/get-metrics-data.ts
new file mode 100644
index 00000000..42c5ffc7
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/pages/get-metrics-data.ts
@@ -0,0 +1,27 @@
+import { useQuery } from "@tanstack/react-query";
+
+import {
+ operationalDashboardMetricsQueryKey,
+ operationalDashboardRefreshMilliseconds,
+} from "../dashboard/dashboard-data";
+import { useDashboardUi } from "../dashboard-ui-provider";
+
+export interface UseGetMetricsDataOptions {
+ enabled?: boolean;
+}
+
+export function useGetMetricsData({
+ enabled = true,
+}: UseGetMetricsDataOptions = {}) {
+ const { dashboard } = useDashboardUi();
+
+ return useQuery({
+ enabled,
+ queryFn: async ({ signal }) => {
+ return dashboard.getOperationalMetrics(signal);
+ },
+ queryKey: operationalDashboardMetricsQueryKey(),
+ refetchInterval: operationalDashboardRefreshMilliseconds,
+ staleTime: operationalDashboardRefreshMilliseconds,
+ });
+}
diff --git a/packages/operational-dashboard-ui/src/pages/operational-dashboard-page.tsx b/packages/operational-dashboard-ui/src/pages/operational-dashboard-page.tsx
new file mode 100644
index 00000000..dd90344f
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/pages/operational-dashboard-page.tsx
@@ -0,0 +1,405 @@
+import {
+ Alert,
+ Bullseye,
+ Content,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateVariant,
+ PageSection,
+ Spinner,
+ Flex,
+ FlexItem,
+ Title,
+} from "@patternfly/react-core";
+import {
+ ClusterIcon,
+ CubesIcon,
+ UsersIcon,
+ MicrochipIcon,
+ MemoryIcon,
+} from "@patternfly/react-icons";
+import {
+ GridLayout,
+ type ExtendedTemplateConfig,
+ type Variants,
+ type WidgetMapping,
+} from "@patternfly/widgetized-dashboard";
+import "@patternfly/widgetized-dashboard/dist/esm/styles.css";
+import { useEffect, useMemo, useState } from "react";
+import { FormattedMessage, useIntl, type IntlShape } from "react-intl";
+
+import type { OperationalDashboardMetrics } from "../application/dashboard-types";
+import type { DashboardProbe } from "../application/dashboard-probes";
+import { noopDashboardProbePublisher } from "../application/dashboard-probes";
+import {
+ defaultDashboardLayoutTemplate,
+ localizeDashboardLayoutTemplate,
+ SUMMARY_WIDGET_HEIGHT,
+} from "../dashboard/dashboard-layout-template";
+import { UtilizationChart } from "../dashboard/utilization-chart";
+import { useDashboardUi } from "../dashboard-ui-provider";
+import { messages } from "../messages";
+import { ResourceRefreshButton } from "../shared/resource-refresh-button";
+import "./dashboard-widget.css";
+import { MetricCard, SummaryCard } from "./dashboard-widget";
+import { useGetMetricsData } from "./get-metrics-data";
+
+const OPERATIONAL_DASHBOARD_BODY_CLASS = "hypershell-operational-dashboard";
+
+const baseTemplate = defaultDashboardLayoutTemplate;
+
+const LAYOUT_STORAGE_KEY = "hypershell.operational-dashboard.layout.v6";
+const CUSTOM_COLUMNS: Record = {
+ xl: 4,
+ lg: 4,
+ md: 4,
+ sm: 1,
+};
+
+function preservesRequiredWidgets(
+ nextTemplate: ExtendedTemplateConfig,
+): boolean {
+ for (const variant of Object.keys(baseTemplate) as Variants[]) {
+ if (!Array.isArray(nextTemplate[variant])) {
+ return false;
+ }
+
+ const requiredIds = new Set(baseTemplate[variant].map((item) => item.i));
+
+ for (const id of requiredIds) {
+ if (!nextTemplate[variant].some((item) => item.i === id)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+function restoreRequiredWidgets(
+ savedTemplate: ExtendedTemplateConfig,
+): ExtendedTemplateConfig {
+ return (Object.keys(baseTemplate) as Variants[]).reduce((acc, variant) => {
+ const savedById = new Map(
+ savedTemplate[variant].map((item) => [item.i, item]),
+ );
+
+ acc[variant] = baseTemplate[variant].map(
+ (defaultItem) => savedById.get(defaultItem.i) ?? defaultItem,
+ );
+
+ return acc;
+ }, {} as ExtendedTemplateConfig);
+}
+
+function layoutProbe(
+ correlationId: string,
+ name: DashboardProbe["name"],
+ outcome: DashboardProbe["fields"]["outcome"],
+): DashboardProbe {
+ return Object.freeze({
+ context: Object.freeze({ correlationId }),
+ fields: Object.freeze({
+ action: "persist-layout-template",
+ outcome,
+ }),
+ name,
+ occurredAt: new Date().toISOString(),
+ schemaVersion: 1,
+ });
+}
+
+const METRIC_WIDGET_DEFAULTS = { h: 3, maxH: 5, minH: 2, w: 1 };
+
+function createWidgetMapping(
+ metrics: OperationalDashboardMetrics,
+ intl: IntlShape,
+): WidgetMapping {
+ const metricById = new Map(
+ metrics.metrics.map((metric) => [metric.id, metric]),
+ );
+
+ const renderMetric = (
+ metricId: string,
+ subtitle: string,
+ titleMessage: (typeof messages)[keyof typeof messages],
+ metricType: "metric" | "utilization",
+ ) => {
+ const metric = metricById.get(metricId);
+ const title = intl.formatMessage(titleMessage);
+
+ if (!metric) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return metricType === "metric" ? (
+
+ ) : (
+
+ );
+ };
+
+ return {
+ summary: {
+ defaults: {
+ h: SUMMARY_WIDGET_HEIGHT,
+ maxH: SUMMARY_WIDGET_HEIGHT + 2,
+ minH: METRIC_WIDGET_DEFAULTS.minH,
+ w: 1,
+ },
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.summary),
+ },
+ renderWidget: () => ,
+ },
+ "active-users": {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.activeUsers),
+ },
+ renderWidget: () =>
+ renderMetric("active-users", "", messages.activeUsers, "metric"),
+ },
+ "provisioned-gateways": {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.provisionedGateways),
+ },
+ renderWidget: () =>
+ renderMetric(
+ "provisioned-gateways",
+ "",
+ messages.provisionedGateways,
+ "metric",
+ ),
+ },
+ "provisioned-sandboxes": {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.provisionedSandboxes),
+ },
+ renderWidget: () =>
+ renderMetric(
+ "provisioned-sandboxes",
+ "",
+ messages.provisionedSandboxes,
+ "metric",
+ ),
+ },
+ namespaces: {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.namespaces),
+ },
+ renderWidget: () =>
+ renderMetric("namespaces", "", messages.namespaces, "metric"),
+ },
+ nodes: {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.nodes),
+ },
+ renderWidget: () => renderMetric("nodes", "", messages.nodes, "metric"),
+ },
+ cpu: {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.widgetCpu),
+ },
+ renderWidget: () =>
+ renderMetric("cpu", "", messages.widgetCpu, "utilization"),
+ },
+ memory: {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.widgetMemory),
+ },
+ renderWidget: () =>
+ renderMetric("memory", "", messages.widgetMemory, "utilization"),
+ },
+ pods: {
+ defaults: METRIC_WIDGET_DEFAULTS,
+ config: {
+ icon: ,
+ title: intl.formatMessage(messages.widgetPods),
+ },
+ renderWidget: () =>
+ renderMetric("pods", "", messages.widgetPods, "utilization"),
+ },
+ };
+}
+
+export interface OperationalDashboardPageProps {
+ metrics?: OperationalDashboardMetrics;
+ title?: string;
+}
+
+export function OperationalDashboardPage({
+ metrics,
+ title,
+}: Readonly) {
+ const intl = useIntl();
+ const { probes = noopDashboardProbePublisher } = useDashboardUi();
+ const pageTitle = title ?? intl.formatMessage(messages.title);
+ const metricsQuery = useGetMetricsData({
+ enabled: metrics === undefined,
+ });
+ const dashboardMetrics = metrics ?? metricsQuery.data;
+ const localizedBaseTemplate = useMemo(
+ () => localizeDashboardLayoutTemplate(baseTemplate, intl),
+ [intl],
+ );
+
+ useEffect(() => {
+ document.body.classList.add(OPERATIONAL_DASHBOARD_BODY_CLASS);
+
+ return () => {
+ document.body.classList.remove(OPERATIONAL_DASHBOARD_BODY_CLASS);
+ };
+ }, []);
+
+ const savedTemplate = useMemo(() => {
+ if (typeof window === "undefined") {
+ return localizedBaseTemplate;
+ }
+
+ try {
+ const rawTemplate = window.localStorage.getItem(LAYOUT_STORAGE_KEY);
+ if (!rawTemplate) {
+ return localizedBaseTemplate;
+ }
+
+ return localizeDashboardLayoutTemplate(
+ restoreRequiredWidgets(
+ JSON.parse(rawTemplate) as ExtendedTemplateConfig,
+ ),
+ intl,
+ );
+ } catch {
+ return localizedBaseTemplate;
+ }
+ }, [intl, localizedBaseTemplate]);
+ const [dashboardTemplate, setDashboardTemplate] =
+ useState(savedTemplate);
+ const [gridLayoutKey, setGridLayoutKey] = useState(0);
+ const displayTemplate = useMemo(
+ () => localizeDashboardLayoutTemplate(dashboardTemplate, intl),
+ [dashboardTemplate, intl],
+ );
+ const widgetMapping = useMemo(
+ () =>
+ dashboardMetrics
+ ? createWidgetMapping(dashboardMetrics, intl)
+ : undefined,
+ [dashboardMetrics, intl],
+ );
+
+ const handleTemplateChange = (nextTemplate: ExtendedTemplateConfig) => {
+ const correlationId = crypto.randomUUID();
+
+ if (!preservesRequiredWidgets(nextTemplate)) {
+ probes.publish(
+ layoutProbe(
+ correlationId,
+ "dashboard.layout.template.invalid",
+ "failed",
+ ),
+ );
+ setGridLayoutKey((currentKey) => currentKey + 1);
+ return;
+ }
+
+ setDashboardTemplate(nextTemplate);
+
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ try {
+ window.localStorage.setItem(
+ LAYOUT_STORAGE_KEY,
+ JSON.stringify(nextTemplate),
+ );
+ } catch {
+ probes.publish(
+ layoutProbe(
+ correlationId,
+ "dashboard.layout.template.persistence-failed",
+ "failed",
+ ),
+ );
+ }
+ };
+
+ return (
+
+
+
+
+ {pageTitle}
+
+
+
+
+
+ {metrics === undefined ? (
+
+ {
+ void metricsQuery.refetch();
+ }}
+ />
+
+ ) : null}
+
+ {metricsQuery.isPending && metrics === undefined ? (
+
+
+
+ ) : null}
+ {metricsQuery.isError && metrics === undefined ? (
+
+ {metricsQuery.error instanceof Error
+ ? metricsQuery.error.message
+ : intl.formatMessage(messages.loadErrorBody)}
+
+ ) : null}
+ {widgetMapping ? (
+
+ ) : null}
+
+ );
+}
diff --git a/packages/operational-dashboard-ui/src/patternfly/victory-charts.ts b/packages/operational-dashboard-ui/src/patternfly/victory-charts.ts
new file mode 100644
index 00000000..9f65a38f
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/patternfly/victory-charts.ts
@@ -0,0 +1,6 @@
+export {
+ ChartArea,
+ ChartGroup,
+ ChartThemeColor,
+ ChartVoronoiContainer,
+} from "@patternfly/react-charts/victory";
diff --git a/packages/operational-dashboard-ui/src/shared/resource-refresh-button.tsx b/packages/operational-dashboard-ui/src/shared/resource-refresh-button.tsx
new file mode 100644
index 00000000..01917c1b
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/shared/resource-refresh-button.tsx
@@ -0,0 +1,29 @@
+import { Button } from "@patternfly/react-core";
+import { SyncAltIcon } from "@patternfly/react-icons";
+
+interface ResourceRefreshButtonProps {
+ ariaLabel: string;
+ isRefreshing?: boolean;
+ onRefresh: () => unknown;
+}
+
+export function ResourceRefreshButton({
+ ariaLabel,
+ isRefreshing = false,
+ onRefresh,
+}: ResourceRefreshButtonProps) {
+ return (
+ }
+ isDisabled={isRefreshing}
+ onClick={() => {
+ onRefresh();
+ }}
+ spinnerAriaValueText={ariaLabel}
+ title={ariaLabel}
+ variant="plain"
+ {...(isRefreshing ? { isLoading: true } : {})}
+ />
+ );
+}
diff --git a/packages/operational-dashboard-ui/src/types.d.ts b/packages/operational-dashboard-ui/src/types.d.ts
new file mode 100644
index 00000000..9e5877ae
--- /dev/null
+++ b/packages/operational-dashboard-ui/src/types.d.ts
@@ -0,0 +1,4 @@
+declare module "*.css" {
+ const classes: Record;
+ export default classes;
+}
diff --git a/packages/operational-dashboard-ui/tsconfig.app.json b/packages/operational-dashboard-ui/tsconfig.app.json
new file mode 100644
index 00000000..b7c40463
--- /dev/null
+++ b/packages/operational-dashboard-ui/tsconfig.app.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "composite": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "lib": ["DOM", "DOM.Iterable", "ES2024"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "noEmit": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noUncheckedIndexedAccess": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2022"
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/operational-dashboard-ui/tsconfig.json b/packages/operational-dashboard-ui/tsconfig.json
new file mode 100644
index 00000000..82a8007e
--- /dev/null
+++ b/packages/operational-dashboard-ui/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9a31ef80..c8fe8a66 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
'@openshift-online/hypershell-gateway-management-ui':
specifier: workspace:0.0.0
version: link:../../packages/gateway-management-ui
+ '@openshift-online/hypershell-operational-dashboard-ui':
+ specifier: workspace:0.0.0
+ version: link:../../packages/operational-dashboard-ui
'@openshift-online/hypershell-sdk':
specifier: workspace:0.0.0
version: link:../sdk-typescript
@@ -44,6 +47,9 @@ importers:
'@opentelemetry/semantic-conventions':
specifier: 1.43.0
version: 1.43.0
+ '@patternfly/react-charts':
+ specifier: 8.6.1
+ version: 8.6.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(victory-area@37.3.6(react@19.2.8))(victory-axis@37.3.6(react@19.2.8))(victory-bar@37.3.6(react@19.2.8))(victory-box-plot@37.3.6(react@19.2.8))(victory-chart@37.3.6(react@19.2.8))(victory-core@37.3.6(react@19.2.8))(victory-create-container@37.3.6(react@19.2.8))(victory-cursor-container@37.3.6(react@19.2.8))(victory-group@37.3.6(react@19.2.8))(victory-legend@37.3.6(react@19.2.8))(victory-line@37.3.6(react@19.2.8))(victory-pie@37.3.6(react@19.2.8))(victory-scatter@37.3.6(react@19.2.8))(victory-stack@37.3.6(react@19.2.8))(victory-tooltip@37.3.6(react@19.2.8))(victory-voronoi-container@37.3.6(react@19.2.8))(victory-zoom-container@37.3.6(react@19.2.8))
'@patternfly/react-core':
specifier: 6.6.0
version: 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -53,6 +59,9 @@ importers:
'@patternfly/react-table':
specifier: 6.6.0
version: 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@patternfly/widgetized-dashboard':
+ specifier: 1.0.0-prerelease.6
+ version: 1.0.0-prerelease.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@react-router/node':
specifier: 8.3.0
version: 8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(typescript@6.0.3)
@@ -77,6 +86,21 @@ importers:
react-router:
specifier: 8.3.0
version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ victory-area:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-core:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-group:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-tooltip:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-voronoi-container:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
web-vitals:
specifier: 5.3.0
version: 5.3.0
@@ -363,6 +387,90 @@ importers:
specifier: 4.4.3
version: 4.4.3
+ packages/operational-dashboard-ui:
+ devDependencies:
+ '@eslint/js':
+ specifier: 9.39.5
+ version: 9.39.5
+ '@openshift-online/hypershell-domain-probes':
+ specifier: workspace:0.0.0
+ version: link:../../components/web-console/domain-probes
+ '@patternfly/react-charts':
+ specifier: 8.6.1
+ version: 8.6.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(victory-area@37.3.6(react@19.2.8))(victory-axis@37.3.6(react@19.2.8))(victory-bar@37.3.6(react@19.2.8))(victory-box-plot@37.3.6(react@19.2.8))(victory-chart@37.3.6(react@19.2.8))(victory-core@37.3.6(react@19.2.8))(victory-create-container@37.3.6(react@19.2.8))(victory-cursor-container@37.3.6(react@19.2.8))(victory-group@37.3.6(react@19.2.8))(victory-legend@37.3.6(react@19.2.8))(victory-line@37.3.6(react@19.2.8))(victory-pie@37.3.6(react@19.2.8))(victory-scatter@37.3.6(react@19.2.8))(victory-stack@37.3.6(react@19.2.8))(victory-tooltip@37.3.6(react@19.2.8))(victory-voronoi-container@37.3.6(react@19.2.8))(victory-zoom-container@37.3.6(react@19.2.8))
+ '@patternfly/react-core':
+ specifier: 6.6.0
+ version: 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@patternfly/react-icons':
+ specifier: 6.6.0
+ version: 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@patternfly/widgetized-dashboard':
+ specifier: 1.0.0-prerelease.6
+ version: 1.0.0-prerelease.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@tanstack/eslint-plugin-query':
+ specifier: 5.101.4
+ version: 5.101.4(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@tanstack/react-query':
+ specifier: 5.101.4
+ version: 5.101.4(react@19.2.8)
+ '@types/react':
+ specifier: 19.2.17
+ version: 19.2.17
+ '@types/react-dom':
+ specifier: 19.2.3
+ version: 19.2.3(@types/react@19.2.17)
+ eslint:
+ specifier: 9.39.5
+ version: 9.39.5(supports-color@7.2.0)
+ eslint-plugin-formatjs:
+ specifier: 6.4.19
+ version: 6.4.19(eslint@9.39.5(supports-color@7.2.0))
+ eslint-plugin-jsx-a11y:
+ specifier: 6.10.2
+ version: 6.10.2(eslint@9.39.5(supports-color@7.2.0))
+ eslint-plugin-react-compiler:
+ specifier: 19.1.0-rc.2
+ version: 19.1.0-rc.2(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-plugin-react-hooks:
+ specifier: 7.1.1
+ version: 7.1.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)
+ globals:
+ specifier: 16.5.0
+ version: 16.5.0
+ prettier:
+ specifier: 3.9.6
+ version: 3.9.6
+ react:
+ specifier: 19.2.8
+ version: 19.2.8
+ react-dom:
+ specifier: 19.2.8
+ version: 19.2.8(react@19.2.8)
+ react-intl:
+ specifier: 10.1.18
+ version: 10.1.18(@types/react@19.2.17)(react@19.2.8)
+ typescript:
+ specifier: 6.0.3
+ version: 6.0.3
+ typescript-eslint:
+ specifier: 8.65.0
+ version: 8.65.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ victory-area:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-core:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-group:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-tooltip:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+ victory-voronoi-container:
+ specifier: 37.3.6
+ version: 37.3.6(react@19.2.8)
+
packages:
'@acemir/cssom@0.9.31':
@@ -1401,6 +1509,67 @@ packages:
cpu: [x64]
os: [win32]
+ '@patternfly/react-charts@8.6.1':
+ resolution: {integrity: sha512-HkYmBanA3KmP/+dw3sbCGITjCif+r1EDzYcfjzLUUshllp55zBMv4UraEpV+pSwSv3fz1SfMQm7Dt5UXL0CWzA==}
+ peerDependencies:
+ echarts: ^5.6.0 || ^6.0.0
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ victory-area: ^37.3.6
+ victory-axis: ^37.3.6
+ victory-bar: ^37.3.6
+ victory-box-plot: ^37.3.6
+ victory-chart: ^37.3.6
+ victory-core: ^37.3.6
+ victory-create-container: ^37.3.6
+ victory-cursor-container: ^37.3.6
+ victory-group: ^37.3.6
+ victory-legend: ^37.3.6
+ victory-line: ^37.3.6
+ victory-pie: ^37.3.6
+ victory-scatter: ^37.3.6
+ victory-stack: ^37.3.6
+ victory-tooltip: ^37.3.6
+ victory-voronoi-container: ^37.3.6
+ victory-zoom-container: ^37.3.6
+ peerDependenciesMeta:
+ echarts:
+ optional: true
+ victory-area:
+ optional: true
+ victory-axis:
+ optional: true
+ victory-bar:
+ optional: true
+ victory-box-plot:
+ optional: true
+ victory-chart:
+ optional: true
+ victory-core:
+ optional: true
+ victory-create-container:
+ optional: true
+ victory-cursor-container:
+ optional: true
+ victory-group:
+ optional: true
+ victory-legend:
+ optional: true
+ victory-line:
+ optional: true
+ victory-pie:
+ optional: true
+ victory-scatter:
+ optional: true
+ victory-stack:
+ optional: true
+ victory-tooltip:
+ optional: true
+ victory-voronoi-container:
+ optional: true
+ victory-zoom-container:
+ optional: true
+
'@patternfly/react-core@6.6.0':
resolution: {integrity: sha512-MLYPcJvdDWHZqWcvEH2QhKtCg6APRfrDJ4o192/wc2LUaf4ViGVsVLKDMbaKCamhbGauv7KpffNCSy3fjRy4Bw==}
peerDependencies:
@@ -1416,6 +1585,9 @@ packages:
'@patternfly/react-styles@6.6.0':
resolution: {integrity: sha512-u/HzJ48Cg6Nb12dIVbj4g31lMDVHPHVKRXkRuPBcuK6MQyAsxSfcyWVivW7Lga8TsNhFW26JWB5ay+IW5xpZ8A==}
+ '@patternfly/react-styles@6.6.1':
+ resolution: {integrity: sha512-hwByXA4wSpOIBJFtrYUdkFDIqvzHRP5yzvip/NHDlG0y/kXausOh5Z4m00SbTA6t7Hzudw5eU1lf+IOPCm5C6Q==}
+
'@patternfly/react-table@6.6.0':
resolution: {integrity: sha512-aOVSAbtXagey1xRI8oIkK3I/csh6S00p42qqOkh6G1s5vMZM7oVQH/Nvvq8gIwx4lZ3f1m7IkdD7My8nml0Bsw==}
peerDependencies:
@@ -1425,6 +1597,15 @@ packages:
'@patternfly/react-tokens@6.6.0':
resolution: {integrity: sha512-oRUFYJAM9LKYTM9YRnd7rz3wB6ZoaBWit/Eu+EK22J4o5AhZRjLt0+ewXQ7I99dBP4c8BbnPEUFnxQYAroqNLg==}
+ '@patternfly/react-tokens@6.6.1':
+ resolution: {integrity: sha512-t7dDEJMnO3QkdIKyjbyKJznVYki9ONZG27Cn/9RXwaM0lDGCrgQEJXy2XoVUCdYhIdXZu+TGSEqXeNFYUyZECg==}
+
+ '@patternfly/widgetized-dashboard@1.0.0-prerelease.6':
+ resolution: {integrity: sha512-1TxqCB1JIyb2cktDsbDn+JqYROPAYFhDo+HQyhNLP+jzuW7ZaMtHZ0gPfZUDyVTxTAjHoKPdnEGnbQ3ZfEUe7g==}
+ peerDependencies:
+ react: ^18 || ^19
+ react-dom: ^18 || ^19
+
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
@@ -1761,6 +1942,33 @@ packages:
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+ '@types/d3-shape@3.1.8':
+ resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
@@ -2208,6 +2416,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -2260,6 +2472,50 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -2326,6 +2582,12 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ delaunator@4.0.1:
+ resolution: {integrity: sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==}
+
+ delaunay-find@0.0.6:
+ resolution: {integrity: sha512-1+almjfrnR7ZamBk0q3Nhg6lqSe6Le4vL0WJDSMx4IDbQwTpUTXPjxC00lqLBT8MYsJpPCbI16sIkw9cPsbi7Q==}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -2531,6 +2793,9 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-equals@4.0.3:
+ resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==}
+
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
@@ -2730,6 +2995,9 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -2782,6 +3050,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
intl-messageformat@11.2.12:
resolution: {integrity: sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==}
@@ -2981,6 +3253,9 @@ packages:
resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==}
engines: {node: '>= 0.4'}
+ json-stringify-safe@5.0.1:
+ resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@@ -3404,12 +3679,27 @@ packages:
peerDependencies:
react: ^19.2.8
+ react-draggable@4.7.1:
+ resolution: {integrity: sha512-wa3tzfFnYt3yaZLuyU58fl1TNunfWfBekDgWhZA1+gb2jnp42wZ0ymuopR6M5kqDYmm4hKmzGlcKWjZf3Zb6RQ==}
+ peerDependencies:
+ react: '>= 16.3.0'
+ react-dom: '>= 16.3.0'
+
react-dropzone@14.4.1:
resolution: {integrity: sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==}
engines: {node: '>= 10.13'}
peerDependencies:
react: '>= 16.8 || 18.0.0'
+ react-fast-compare@3.2.2:
+ resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
+ react-grid-layout@2.2.4:
+ resolution: {integrity: sha512-Eb57FsgOMYOfsUGrMI1ku/FFR+dPNPrE8qo+3hwZubpqVSy4GO9v52DeX50Tl3JDYAlCypP4rmw7Vrqk/zOIvA==}
+ peerDependencies:
+ react: '>= 16.3.0'
+ react-dom: '>= 16.3.0'
+
react-hook-form@7.82.0:
resolution: {integrity: sha512-Zw/uFZ2dO+02GHlBn7JFGn8kZJ7LdM33B/0BXOovzFay+CMhf94JMw5BVu+F1tVkUKjNvBuaE3fz5BJhga10Tg==}
engines: {node: '>=18.0.0'}
@@ -3432,6 +3722,12 @@ packages:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
+ react-resizable@3.2.0:
+ resolution: {integrity: sha512-3NKQ0SLZV7rs3LQHeXlOzDSRQfFrkX6TVet77/Qk03zqiZyee37b7N8/gwDJAA8UUjRz7PdWCCy49hcso45SMQ==}
+ peerDependencies:
+ react: '>= 16.3'
+ react-dom: '>= 16.3'
+
react-router@8.3.0:
resolution: {integrity: sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==}
engines: {node: '>=22.22.0'}
@@ -3498,6 +3794,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ resize-observer-polyfill@1.5.1:
+ resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -3926,6 +4225,135 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-area@37.3.6:
+ resolution: {integrity: sha512-wVC8LKrZJLiSySNuJLRCB449qZTsPiRyzLlNoJwe21y+XA/a2HJbmJSeywmo8P153aX8viKe1H8ygDsTFXQhHw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-axis@37.3.6:
+ resolution: {integrity: sha512-Vi0dZvgmXmnCdoqc49WckeG5cMXnl7FTtqVhXu9JweA9cgCnkZabBd5mRvAjblb3Lo4j0HZCSPKHYWUPW70qZg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-bar@37.3.6:
+ resolution: {integrity: sha512-jdATFRWL1LUW/yEpKWx/aId2BiU2o1pPF9+Kh1TFISBduJoI4ZqvZD90H1QK4f/z50PikqiqiDECaKoKM1jfOQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-box-plot@37.3.6:
+ resolution: {integrity: sha512-GOucnD63h14ScBuISC/nd1GBTEx6gIZfLE+0P0gyeH1poBKq0trTTvpQDvAMuGR8zICfEETG3ltmUMCwRrFyUg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-brush-container@37.3.6:
+ resolution: {integrity: sha512-LfZ2CgX1cYAqCtYxcSB68OfZS2v0T2VLXoEArd0lCXfRBY1Gya7GacCUcuo7GoK9XOXeslx7S/U95aVutt1VLg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-chart@37.3.6:
+ resolution: {integrity: sha512-IkPo/W4AJ7bPu902TGER09OseR9ODm+FQAKfOBw4JsdEhZZ7BiG9zgd/25+x0r5EsTLu81CYGQVkBa+ZazcOlA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-core@37.3.6:
+ resolution: {integrity: sha512-aFgO6KokxPbUCPznZP5UPhOdI22pMuwDXKDt6eoQOnkVim66Ia+K95TQar2nwVKGYV5j26aKVf/n9blwphGJRw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-create-container@37.3.6:
+ resolution: {integrity: sha512-Uf5bFQvqUsXCjqpvBW4LhrdrHkM6dBqxYgub6FCsBb86f84xZQ3vY7jFkg/JfvF0oGKMoWXYYrYLC1sk+fcWVA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-cursor-container@37.3.6:
+ resolution: {integrity: sha512-+Oiw57d5nE+iq8As8RvepknzmNtKq1Gsc50u1X3IRd4jXtX8zqZrgXGlVZ+BP/tkLsWnGYVjKulwKBf2oaEUuw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-group@37.3.6:
+ resolution: {integrity: sha512-kgy/Azl5BxwlJAV0KDPGypv35TMrOD1J2ZxnJW2Wyyq+e8i0GGBIv5MoBzou64BRsDlS9V0CYRIjnkHgrBpB5w==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-legend@37.3.6:
+ resolution: {integrity: sha512-vRRrhj3/ENqKVLdaBMzEmR83N6BOjox1bthYT1eJjN2H5SIK35bxn30IkiV/Pz3y627EqZe4TAWaxc0jiJlCiA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-line@37.3.6:
+ resolution: {integrity: sha512-Ke817uf/qFbN9jU7Dba7CrcHXYO5wAZuKKnyeHJmLDeQeFST0773xejnIuC+dBgZipjFr4KIbSd+VcUafFNE1g==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-pie@37.3.6:
+ resolution: {integrity: sha512-tvdgAZ/HQWlo3KDDe0XAVbizHuaNMbgkkiF7zfA7Ww+3bHSs+0P9dsDtK2xP365D8gBCOv8pWmuzvKRhzNbqeA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-polar-axis@37.3.6:
+ resolution: {integrity: sha512-RpFsCkzHezJq5P+C/wtVdjEHX25JIFsSgs6qYSnfr/hayaFbWgK5HhRFpriQm5hg61cx47WxAOLyHvzf0nasvw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-scatter@37.3.6:
+ resolution: {integrity: sha512-fp95zMTPXgW1cmTowzDXhn+KxePMVDrzU0lotsHQMdBV7eB+ioXdu9hORlx4VHmMYg2ihsGwRTF+VAZ7rGxphA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-selection-container@37.3.6:
+ resolution: {integrity: sha512-gd3qODDlBtLEJM7+2jCXk2YcLBUmIpYEEHswytMhwc6zihxXipGBUHRulhLj/I05mKay2gaOAg5ewiJHd4Awgw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-shared-events@37.3.6:
+ resolution: {integrity: sha512-ygrbOtzLUTbtKebacZKyQRekhSAROnAvMkVI/PKsAGsz0ClY9P7qDEJG7eTUUygjO6ax0tI6WNE6JogQzeD1gw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-stack@37.3.6:
+ resolution: {integrity: sha512-ldod04RdqGJGH5p5eWXCofdTkbhZqIp3iwW7NpxSbMDLs8zPQIVvDFVtuJgMwQiC5vnIpbhMmxVeFbr8m64ZKA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-tooltip@37.3.6:
+ resolution: {integrity: sha512-vqaJS9noauOqDDBBAV9Ln9duOY/i17h1DCfCPAqhwPFyvFbwKvAub9zPTeYWAm/14VvWX5O/0yekFCVbcC7hjg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-vendor@37.3.6:
+ resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
+
+ victory-voronoi-container@37.3.6:
+ resolution: {integrity: sha512-qAAG0rMuK7A4EoJ4cyUk5wNdOW+HuCXNKPOko+hYK6wWOYXJvFhiglYyA85a695YyAXECc6JyJS/crm4IOEFag==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
+ victory-zoom-container@37.3.6:
+ resolution: {integrity: sha512-AGL+k20mI44OL5b0VgIxlmnNSefIoFmbbim5NraPmIxbtns9qQW/56ivIncJcYomBungIx99gUpsEpcQaMNHgQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: '>=16.6.0'
+
vite@8.1.5:
resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -5001,6 +5429,34 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.24.2':
optional: true
+ '@patternfly/react-charts@8.6.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(victory-area@37.3.6(react@19.2.8))(victory-axis@37.3.6(react@19.2.8))(victory-bar@37.3.6(react@19.2.8))(victory-box-plot@37.3.6(react@19.2.8))(victory-chart@37.3.6(react@19.2.8))(victory-core@37.3.6(react@19.2.8))(victory-create-container@37.3.6(react@19.2.8))(victory-cursor-container@37.3.6(react@19.2.8))(victory-group@37.3.6(react@19.2.8))(victory-legend@37.3.6(react@19.2.8))(victory-line@37.3.6(react@19.2.8))(victory-pie@37.3.6(react@19.2.8))(victory-scatter@37.3.6(react@19.2.8))(victory-stack@37.3.6(react@19.2.8))(victory-tooltip@37.3.6(react@19.2.8))(victory-voronoi-container@37.3.6(react@19.2.8))(victory-zoom-container@37.3.6(react@19.2.8))':
+ dependencies:
+ '@patternfly/react-styles': 6.6.1
+ '@patternfly/react-tokens': 6.6.1
+ hoist-non-react-statics: 3.3.2
+ lodash: 4.18.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ tslib: 2.8.1
+ optionalDependencies:
+ victory-area: 37.3.6(react@19.2.8)
+ victory-axis: 37.3.6(react@19.2.8)
+ victory-bar: 37.3.6(react@19.2.8)
+ victory-box-plot: 37.3.6(react@19.2.8)
+ victory-chart: 37.3.6(react@19.2.8)
+ victory-core: 37.3.6(react@19.2.8)
+ victory-create-container: 37.3.6(react@19.2.8)
+ victory-cursor-container: 37.3.6(react@19.2.8)
+ victory-group: 37.3.6(react@19.2.8)
+ victory-legend: 37.3.6(react@19.2.8)
+ victory-line: 37.3.6(react@19.2.8)
+ victory-pie: 37.3.6(react@19.2.8)
+ victory-scatter: 37.3.6(react@19.2.8)
+ victory-stack: 37.3.6(react@19.2.8)
+ victory-tooltip: 37.3.6(react@19.2.8)
+ victory-voronoi-container: 37.3.6(react@19.2.8)
+ victory-zoom-container: 37.3.6(react@19.2.8)
+
'@patternfly/react-core@6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@patternfly/react-icons': 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -5020,6 +5476,8 @@ snapshots:
'@patternfly/react-styles@6.6.0': {}
+ '@patternfly/react-styles@6.6.1': {}
+
'@patternfly/react-table@6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@patternfly/react-core': 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -5033,6 +5491,17 @@ snapshots:
'@patternfly/react-tokens@6.6.0': {}
+ '@patternfly/react-tokens@6.6.1': {}
+
+ '@patternfly/widgetized-dashboard@1.0.0-prerelease.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@patternfly/react-core': 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@patternfly/react-icons': 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ clsx: 2.1.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ react-grid-layout: 2.2.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+
'@pinojs/redact@0.4.0': {}
'@playwright/test@1.61.1':
@@ -5349,6 +5818,30 @@ snapshots:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-shape@3.1.8':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/deep-eql@4.0.2': {}
'@types/doctrine@0.0.9': {}
@@ -5846,6 +6339,8 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -5890,6 +6385,44 @@ snapshots:
csstype@3.2.3: {}
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-color@3.1.0: {}
+
+ d3-ease@3.0.1: {}
+
+ d3-format@3.1.2: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@3.1.0: {}
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
damerau-levenshtein@1.0.8: {}
data-urls@6.0.1:
@@ -5950,6 +6483,12 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ delaunator@4.0.1: {}
+
+ delaunay-find@0.0.6:
+ dependencies:
+ delaunator: 4.0.1
+
depd@2.0.0: {}
dequal@2.0.3: {}
@@ -6264,6 +6803,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
+ fast-equals@4.0.3: {}
+
fast-fifo@1.3.2: {}
fast-json-stable-stringify@2.1.0: {}
@@ -6489,6 +7030,10 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
html-encoding-sniffer@6.0.0:
dependencies:
'@exodus/bytes': 1.15.1
@@ -6544,6 +7089,8 @@ snapshots:
hasown: 2.0.4
side-channel: 1.1.1
+ internmap@2.0.3: {}
+
intl-messageformat@11.2.12:
dependencies:
'@formatjs/fast-memoize': 3.1.7
@@ -6756,6 +7303,8 @@ snapshots:
jsonify: 0.0.1
object-keys: 1.1.1
+ json-stringify-safe@5.0.1: {}
+
json5@2.2.3: {}
jsonc-parser@3.3.1: {}
@@ -7214,6 +7763,13 @@ snapshots:
react: 19.2.8
scheduler: 0.27.0
+ react-draggable@4.7.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+
react-dropzone@14.4.1(react@19.2.8):
dependencies:
attr-accept: 2.2.5
@@ -7221,6 +7777,19 @@ snapshots:
prop-types: 15.8.1
react: 19.2.8
+ react-fast-compare@3.2.2: {}
+
+ react-grid-layout@2.2.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ clsx: 2.1.1
+ fast-equals: 4.0.3
+ prop-types: 15.8.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ react-draggable: 4.7.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ react-resizable: 3.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ resize-observer-polyfill: 1.5.1
+
react-hook-form@7.82.0(react@19.2.8):
dependencies:
react: 19.2.8
@@ -7239,6 +7808,13 @@ snapshots:
react-refresh@0.18.0: {}
+ react-resizable@3.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ dependencies:
+ prop-types: 15.8.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ react-draggable: 4.7.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+
react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
cookie-es: 3.1.1
@@ -7315,6 +7891,8 @@ snapshots:
require-from-string@2.0.2: {}
+ resize-observer-polyfill@1.5.1: {}
+
resolve-from@4.0.0: {}
resolve@1.22.12:
@@ -7812,6 +8390,189 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ victory-area@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ victory-vendor: 37.3.6
+
+ victory-axis@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-bar@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ victory-vendor: 37.3.6
+ optional: true
+
+ victory-box-plot@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ victory-vendor: 37.3.6
+ optional: true
+
+ victory-brush-container@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-chart@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-axis: 37.3.6(react@19.2.8)
+ victory-core: 37.3.6(react@19.2.8)
+ victory-polar-axis: 37.3.6(react@19.2.8)
+ victory-shared-events: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-core@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-vendor: 37.3.6
+
+ victory-create-container@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-brush-container: 37.3.6(react@19.2.8)
+ victory-core: 37.3.6(react@19.2.8)
+ victory-cursor-container: 37.3.6(react@19.2.8)
+ victory-selection-container: 37.3.6(react@19.2.8)
+ victory-voronoi-container: 37.3.6(react@19.2.8)
+ victory-zoom-container: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-cursor-container@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-group@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-core: 37.3.6(react@19.2.8)
+ victory-shared-events: 37.3.6(react@19.2.8)
+
+ victory-legend@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-line@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ victory-vendor: 37.3.6
+ optional: true
+
+ victory-pie@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ victory-vendor: 37.3.6
+ optional: true
+
+ victory-polar-axis@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-scatter@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-selection-container@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-shared-events@37.3.6(react@19.2.8):
+ dependencies:
+ json-stringify-safe: 5.0.1
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-core: 37.3.6(react@19.2.8)
+
+ victory-stack@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-core: 37.3.6(react@19.2.8)
+ victory-shared-events: 37.3.6(react@19.2.8)
+ optional: true
+
+ victory-tooltip@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+
+ victory-vendor@37.3.6:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
+ victory-voronoi-container@37.3.6(react@19.2.8):
+ dependencies:
+ delaunay-find: 0.0.6
+ lodash: 4.18.1
+ react: 19.2.8
+ react-fast-compare: 3.2.2
+ victory-core: 37.3.6(react@19.2.8)
+ victory-tooltip: 37.3.6(react@19.2.8)
+
+ victory-zoom-container@37.3.6(react@19.2.8):
+ dependencies:
+ lodash: 4.18.1
+ react: 19.2.8
+ victory-core: 37.3.6(react@19.2.8)
+ optional: true
+
vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1):
dependencies:
lightningcss: 1.33.0
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 168a30aa..d74353af 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,6 +1,7 @@
packages:
- components/sdk-typescript
- packages/gateway-management-ui
+ - packages/operational-dashboard-ui
- components/web-console
- components/web-console/bff
- components/web-console/domain-probes
diff --git a/scripts/kind/swap-component.sh b/scripts/kind/swap-component.sh
index 226c6ffe..6f98f61a 100755
--- a/scripts/kind/swap-component.sh
+++ b/scripts/kind/swap-component.sh
@@ -256,10 +256,11 @@ EOF
(cd "${REPO_ROOT}" && \
info "Installing workspace dependencies (pnpm install)..." && \
pnpm install --frozen-lockfile --reporter=append-only && \
- info "Building workspace dependencies (sdk → domain-probes → gateway-management-ui)..." && \
+ info "Building workspace dependencies (sdk → domain-probes → gateway-management-ui → operational-dashboard-ui)..." && \
pnpm --filter @openshift-online/hypershell-sdk build && \
pnpm --filter @openshift-online/hypershell-domain-probes build && \
pnpm --filter @openshift-online/hypershell-gateway-management-ui build && \
+ pnpm --filter @openshift-online/hypershell-operational-dashboard-ui build && \
info "Starting dev server (first run optimizes dependencies, up to ~1 min with no output; Ctrl+C to stop and revert)..." && \
DEV_SERVER_HOST=0.0.0.0 pnpm --filter @openshift-online/hypershell-web-console dev) || true
exit 0
@@ -267,6 +268,15 @@ EOF
header "Swap ${COMPONENT} (up)"
+ if [[ "${COMPONENT}" == "web-console" ]]; then
+ # A prior hot-reload session leaves a hand-written EndpointSlice and may
+ # strip the Service selector. Remove those before deploying the in-cluster
+ # image or the gateway load-balances to a dead host:5173 backend (503).
+ kube delete endpointslices "${DEPLOYMENT}" -n "${KIND_NAMESPACE}" 2>/dev/null || true
+ kube delete endpoints "${DEPLOYMENT}" -n "${KIND_NAMESPACE}" 2>/dev/null || true
+ restore_web_console_from_overlay || true
+ fi
+
info "Building ${COMPONENT} from working tree..."
${CONTAINER_ENGINE} build -t "${LOCAL_IMAGE}" \
-f "${DOCKERFILE}" ${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"} "${BUILD_CONTEXT}"
@@ -320,6 +330,7 @@ swap_down() {
info "Reverting ${COMPONENT} to baseline image..."
if [[ "${COMPONENT}" == "web-console" ]]; then
+ kube delete endpointslices "${DEPLOYMENT}" -n "${KIND_NAMESPACE}" 2>/dev/null || true
kube delete endpoints "${DEPLOYMENT}" -n "${KIND_NAMESPACE}" 2>/dev/null || true
restore_web_console_from_overlay
else