@echothink-ui/admin 0.1.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 (48) hide show
  1. package/README.md +5 -0
  2. package/dist/components/AdminHealthTemplate.d.ts +5 -0
  3. package/dist/components/AdminShell.d.ts +8 -0
  4. package/dist/components/AuditLogViewer.d.ts +9 -0
  5. package/dist/components/BillingUsagePanel.d.ts +7 -0
  6. package/dist/components/FeatureFlagPanel.d.ts +8 -0
  7. package/dist/components/IntegrationHealthTable.d.ts +6 -0
  8. package/dist/components/JobQueuePanel.d.ts +7 -0
  9. package/dist/components/PolicyConfigPanel.d.ts +9 -0
  10. package/dist/components/QueueDepthChart.d.ts +6 -0
  11. package/dist/components/RateLimitPanel.d.ts +7 -0
  12. package/dist/components/RuntimeLogViewer.d.ts +10 -0
  13. package/dist/components/ServiceStatusCard.d.ts +7 -0
  14. package/dist/components/SystemHealthDashboard.d.ts +8 -0
  15. package/dist/components/TenantSettingsPanel.d.ts +11 -0
  16. package/dist/components/WorkerPoolPanel.d.ts +7 -0
  17. package/dist/components/types.d.ts +114 -0
  18. package/dist/index.cjs +1835 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.css +1711 -0
  21. package/dist/index.css.map +1 -0
  22. package/dist/index.d.ts +19 -0
  23. package/dist/index.js +1812 -0
  24. package/dist/index.js.map +1 -0
  25. package/package.json +46 -0
  26. package/src/components/AdminHealthTemplate.tsx +25 -0
  27. package/src/components/AdminShell.tsx +48 -0
  28. package/src/components/AuditLogViewer.tsx +96 -0
  29. package/src/components/BillingUsagePanel.tsx +171 -0
  30. package/src/components/FeatureFlagPanel.tsx +178 -0
  31. package/src/components/IntegrationHealthTable.tsx +196 -0
  32. package/src/components/JobQueuePanel.tsx +171 -0
  33. package/src/components/PolicyConfigPanel.test.tsx +45 -0
  34. package/src/components/PolicyConfigPanel.tsx +131 -0
  35. package/src/components/QueueDepthChart.tsx +70 -0
  36. package/src/components/RateLimitPanel.test.tsx +29 -0
  37. package/src/components/RateLimitPanel.tsx +249 -0
  38. package/src/components/RuntimeLogViewer.test.tsx +60 -0
  39. package/src/components/RuntimeLogViewer.tsx +185 -0
  40. package/src/components/ServiceStatusCard.tsx +91 -0
  41. package/src/components/SystemHealthDashboard.tsx +214 -0
  42. package/src/components/TenantSettingsPanel.test.tsx +38 -0
  43. package/src/components/TenantSettingsPanel.tsx +44 -0
  44. package/src/components/WorkerPoolPanel.test.tsx +32 -0
  45. package/src/components/WorkerPoolPanel.tsx +281 -0
  46. package/src/components/types.ts +131 -0
  47. package/src/index.tsx +37 -0
  48. package/src/styles.css +2024 -0
@@ -0,0 +1,60 @@
1
+ import { fireEvent, render, screen, within } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { RuntimeLogViewer } from "./RuntimeLogViewer";
4
+
5
+ const entries = [
6
+ {
7
+ id: "1",
8
+ timestamp: "10:42:01",
9
+ level: "info" as const,
10
+ source: "ingest",
11
+ message: "Started batch ingest."
12
+ },
13
+ {
14
+ id: "2",
15
+ timestamp: "10:42:04",
16
+ level: "warning" as const,
17
+ source: "render",
18
+ message: "Latency 1.2s above threshold."
19
+ },
20
+ {
21
+ id: "3",
22
+ timestamp: "10:42:08",
23
+ level: "error" as const,
24
+ source: "billing",
25
+ message: "5xx from upstream."
26
+ }
27
+ ];
28
+
29
+ describe("RuntimeLogViewer", () => {
30
+ it("renders streaming log controls, status, filters, and summary counts", () => {
31
+ const onPause = vi.fn();
32
+
33
+ render(
34
+ <RuntimeLogViewer
35
+ entries={entries}
36
+ streaming
37
+ onPause={onPause}
38
+ filters={<span>Source: billing</span>}
39
+ />
40
+ );
41
+
42
+ const viewer = screen.getByRole("region", { name: "Runtime logs" });
43
+
44
+ expect(within(viewer).getByText("Streaming")).toBeTruthy();
45
+ expect(within(viewer).getByText("3 entries")).toBeTruthy();
46
+ expect(within(viewer).getByText("1 warning")).toBeTruthy();
47
+ expect(within(viewer).getAllByText("1 error")).toHaveLength(2);
48
+ expect(within(viewer).getByText("Source: billing")).toBeTruthy();
49
+
50
+ fireEvent.click(within(viewer).getByRole("button", { name: "Pause runtime stream" }));
51
+
52
+ expect(onPause).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it("does not render an inert pause action without a handler", () => {
56
+ render(<RuntimeLogViewer entries={entries} streaming />);
57
+
58
+ expect(screen.queryByRole("button", { name: "Pause runtime stream" })).toBeNull();
59
+ });
60
+ });
@@ -0,0 +1,185 @@
1
+ import * as React from "react";
2
+ import { Pause } from "@carbon/icons-react";
3
+ import { Badge, Button, type EthSeverity, type SurfaceComponentProps } from "@echothink-ui/core";
4
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
5
+ import type { LogEntry } from "./types";
6
+
7
+ export interface RuntimeLogViewerProps extends Omit<SurfaceComponentProps, "children"> {
8
+ entries: LogEntry[];
9
+ filters?: React.ReactNode;
10
+ onPause?: () => void;
11
+ streaming?: boolean;
12
+ }
13
+
14
+ type LogRow = Record<string, unknown> & LogEntry;
15
+
16
+ export function RuntimeLogViewer({
17
+ entries,
18
+ filters,
19
+ onPause,
20
+ streaming = false,
21
+ className,
22
+ title = "Runtime logs",
23
+ subtitle,
24
+ description,
25
+ density = "compact",
26
+ "aria-label": ariaLabel,
27
+ "aria-labelledby": ariaLabelledBy
28
+ }: RuntimeLogViewerProps) {
29
+ const headingId = React.useId();
30
+ const warningCount = countLevel(entries, "warning");
31
+ const errorCount = countLevel(entries, "error");
32
+ const latestTimestamp = entries[entries.length - 1]?.timestamp ?? "No activity";
33
+ const supportingText =
34
+ subtitle ?? description ?? `${formatEntryCount(entries.length)} buffered from runtime stream.`;
35
+ const hasToolbar = Boolean(filters || (streaming && onPause));
36
+ const rows: LogRow[] = entries.map((entry) => ({ ...entry }));
37
+ const columns: DataColumn<LogRow>[] = [
38
+ {
39
+ key: "timestamp",
40
+ header: "Time",
41
+ width: "7rem",
42
+ render: (row) => (
43
+ <time className="eth-admin-runtime-log__timestamp" dateTime={row.timestamp}>
44
+ {row.timestamp}
45
+ </time>
46
+ )
47
+ },
48
+ {
49
+ key: "level",
50
+ header: "Level",
51
+ width: "7rem",
52
+ render: (row) => (
53
+ <Badge severity={levelSeverity(row.level)}>{formatLevel(row.level)}</Badge>
54
+ )
55
+ },
56
+ {
57
+ key: "source",
58
+ header: "Source",
59
+ width: "11rem",
60
+ render: (row) => (
61
+ <span className="eth-admin-runtime-log__source">{row.source ?? "system"}</span>
62
+ )
63
+ },
64
+ {
65
+ key: "message",
66
+ header: "Message",
67
+ render: (row) => (
68
+ <span
69
+ className={[
70
+ "eth-admin-runtime-log__message",
71
+ `eth-admin-runtime-log__message--${row.level ?? "info"}`
72
+ ].join(" ")}
73
+ >
74
+ {row.message}
75
+ </span>
76
+ )
77
+ }
78
+ ];
79
+
80
+ return (
81
+ <section
82
+ role="region"
83
+ aria-label={ariaLabel}
84
+ aria-labelledby={ariaLabelledBy ?? (ariaLabel ? undefined : headingId)}
85
+ className={[
86
+ "eth-admin-runtime-log",
87
+ streaming ? "eth-admin-runtime-log--streaming" : "eth-admin-runtime-log--paused",
88
+ className
89
+ ]
90
+ .filter(Boolean)
91
+ .join(" ")}
92
+ data-eth-component="RuntimeLogViewer"
93
+ >
94
+ <header className="eth-admin-runtime-log__header">
95
+ <div className="eth-admin-runtime-log__heading">
96
+ <h3 id={headingId}>{title}</h3>
97
+ <p>{supportingText}</p>
98
+ </div>
99
+ <div className="eth-admin-runtime-log__status" aria-live="polite">
100
+ <Badge severity={streaming ? "success" : "neutral"}>
101
+ {streaming ? "Streaming" : "Paused"}
102
+ </Badge>
103
+ <span>{errorCount ? formatLevelCount(errorCount, "error") : "No errors"}</span>
104
+ </div>
105
+ </header>
106
+ {hasToolbar ? (
107
+ <div
108
+ className="eth-admin-runtime-log__toolbar"
109
+ role="toolbar"
110
+ aria-label="Runtime log controls"
111
+ >
112
+ {filters ? <div className="eth-admin-runtime-log__filters">{filters}</div> : null}
113
+ {streaming && onPause ? (
114
+ <Button
115
+ type="button"
116
+ intent="secondary"
117
+ density="compact"
118
+ icon={<Pause size={16} />}
119
+ aria-label="Pause runtime stream"
120
+ onClick={onPause}
121
+ >
122
+ Pause stream
123
+ </Button>
124
+ ) : null}
125
+ </div>
126
+ ) : null}
127
+ <dl className="eth-admin-runtime-log__summary" aria-label="Runtime log summary">
128
+ <div>
129
+ <dt>Entries</dt>
130
+ <dd>{formatEntryCount(entries.length)}</dd>
131
+ </div>
132
+ <div>
133
+ <dt>Warnings</dt>
134
+ <dd>{formatLevelCount(warningCount, "warning")}</dd>
135
+ </div>
136
+ <div>
137
+ <dt>Errors</dt>
138
+ <dd>{formatLevelCount(errorCount, "error")}</dd>
139
+ </div>
140
+ <div>
141
+ <dt>Latest</dt>
142
+ <dd>{latestTimestamp}</dd>
143
+ </div>
144
+ </dl>
145
+ <div className="eth-admin-runtime-log__table-shell" aria-live={streaming ? "polite" : "off"}>
146
+ <DataTable
147
+ rows={rows}
148
+ columns={columns}
149
+ density={density}
150
+ rowKey="id"
151
+ className="eth-admin-runtime-log__table"
152
+ emptyState={
153
+ <div className="eth-admin-runtime-log__empty" role="status">
154
+ <h4>No runtime logs</h4>
155
+ <p>Events will appear here when the stream receives output.</p>
156
+ </div>
157
+ }
158
+ />
159
+ </div>
160
+ </section>
161
+ );
162
+ }
163
+
164
+ function countLevel(entries: LogEntry[], level: NonNullable<LogEntry["level"]>) {
165
+ return entries.filter((entry) => entry.level === level).length;
166
+ }
167
+
168
+ function formatEntryCount(count: number) {
169
+ return `${count} ${count === 1 ? "entry" : "entries"}`;
170
+ }
171
+
172
+ function formatLevelCount(count: number, label: string) {
173
+ return `${count} ${count === 1 ? label : `${label}s`}`;
174
+ }
175
+
176
+ function formatLevel(level: LogEntry["level"]) {
177
+ return (level ?? "info").toUpperCase();
178
+ }
179
+
180
+ function levelSeverity(level: LogEntry["level"]): EthSeverity {
181
+ if (level === "error") return "danger";
182
+ if (level === "warning") return "warning";
183
+ if (level === "debug") return "neutral";
184
+ return "info";
185
+ }
@@ -0,0 +1,91 @@
1
+ import { Badge, Surface, type EthSeverity, type SurfaceComponentProps } from "@echothink-ui/core";
2
+ import type { ServiceHealthStatus, ServiceStatusSummary } from "./types";
3
+
4
+ const percentFormatter = new Intl.NumberFormat("en", { maximumFractionDigits: 2 });
5
+
6
+ export interface ServiceStatusCardProps extends Omit<SurfaceComponentProps, "children"> {
7
+ service: ServiceStatusSummary;
8
+ }
9
+
10
+ export function ServiceStatusCard({ service, className, ...props }: ServiceStatusCardProps) {
11
+ const statusSeverity = severityForStatus(service.status);
12
+ const statusText = statusLabel(service.status);
13
+ const metrics = [
14
+ {
15
+ label: "Latency",
16
+ value: service.latencyMs !== undefined ? `${service.latencyMs} ms` : "Unavailable",
17
+ muted: service.latencyMs === undefined
18
+ },
19
+ {
20
+ label: "Uptime",
21
+ value: service.uptimePercent !== undefined ? formatPercent(service.uptimePercent) : "Not reported",
22
+ muted: service.uptimePercent === undefined
23
+ },
24
+ {
25
+ label: service.incidentsCount !== undefined ? "Open incidents" : "Last incident",
26
+ value:
27
+ service.incidentsCount !== undefined
28
+ ? service.incidentsCount
29
+ : service.lastIncidentAt ?? "None reported",
30
+ muted:
31
+ service.incidentsCount !== undefined ? service.incidentsCount === 0 : !service.lastIncidentAt
32
+ }
33
+ ];
34
+
35
+ return (
36
+ <Surface
37
+ {...props}
38
+ title={
39
+ <span className="eth-admin-service-status-card__title">
40
+ <span>{service.name}</span>
41
+ <Badge aria-label={`Service status: ${statusText}`} severity={statusSeverity}>
42
+ {statusText}
43
+ </Badge>
44
+ </span>
45
+ }
46
+ description={statusDescription(service.status)}
47
+ className={[
48
+ "eth-admin-service-status-card",
49
+ `eth-admin-service-status-card--${service.status}`,
50
+ className
51
+ ]
52
+ .filter(Boolean)
53
+ .join(" ")}
54
+ data-eth-component="ServiceStatusCard"
55
+ >
56
+ <dl className="eth-admin-service-status-card__metrics">
57
+ {metrics.map((metric) => (
58
+ <div
59
+ key={metric.label}
60
+ className={metric.muted ? "eth-admin-service-status-card__metric--muted" : undefined}
61
+ >
62
+ <dt>{metric.label}</dt>
63
+ <dd>{metric.value}</dd>
64
+ </div>
65
+ ))}
66
+ </dl>
67
+ </Surface>
68
+ );
69
+ }
70
+
71
+ export function severityForStatus(status: ServiceHealthStatus): EthSeverity {
72
+ if (status === "healthy") return "success";
73
+ if (status === "degraded") return "warning";
74
+ return "danger";
75
+ }
76
+
77
+ function statusLabel(status: ServiceHealthStatus) {
78
+ if (status === "healthy") return "Healthy";
79
+ if (status === "degraded") return "Degraded";
80
+ return "Down";
81
+ }
82
+
83
+ function statusDescription(status: ServiceHealthStatus) {
84
+ if (status === "healthy") return "Operational within expected thresholds";
85
+ if (status === "degraded") return "Elevated latency or partial failures detected";
86
+ return "Service unavailable or actively failing";
87
+ }
88
+
89
+ function formatPercent(value: number) {
90
+ return `${percentFormatter.format(value)}%`;
91
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ Badge,
3
+ Surface,
4
+ type EthOperationalStatus,
5
+ type EthSeverity,
6
+ type SurfaceComponentProps
7
+ } from "@echothink-ui/core";
8
+ import { KPIBlock } from "@echothink-ui/charts";
9
+ import { ServiceStatusCard } from "./ServiceStatusCard";
10
+ import type { AdminIncident, AdminMetric, HealthService } from "./types";
11
+
12
+ export interface SystemHealthDashboardProps extends Omit<SurfaceComponentProps, "children"> {
13
+ services: HealthService[];
14
+ incidents?: AdminIncident[];
15
+ metrics?: AdminMetric[];
16
+ }
17
+
18
+ export function SystemHealthDashboard({
19
+ services,
20
+ incidents = [],
21
+ metrics = [],
22
+ title,
23
+ description,
24
+ severity,
25
+ status,
26
+ className,
27
+ ...props
28
+ }: SystemHealthDashboardProps) {
29
+ const serviceCount = services.length;
30
+ const healthyCount = services.filter((service) => service.status === "healthy").length;
31
+ const degradedCount = services.filter((service) => service.status === "degraded").length;
32
+ const downCount = services.filter((service) => service.status === "down").length;
33
+ const criticalIncidentCount = incidents.filter(
34
+ (incident) => incident.severity === "critical" || incident.severity === "error"
35
+ ).length;
36
+ const openIncidentCount = incidents.length;
37
+ const defaultSeverity = severityForHealth(
38
+ downCount,
39
+ degradedCount,
40
+ criticalIncidentCount,
41
+ openIncidentCount
42
+ );
43
+ const defaultStatus = statusForHealth(serviceCount, downCount, degradedCount);
44
+ const computedMetrics = metrics.length
45
+ ? metrics
46
+ : [
47
+ {
48
+ label: "Healthy services",
49
+ value: serviceCount ? `${healthyCount} / ${serviceCount}` : "0",
50
+ trend: serviceHealthTrend(healthyCount, degradedCount, downCount)
51
+ },
52
+ {
53
+ label: "Open incidents",
54
+ value: openIncidentCount,
55
+ trend: incidentTrend(criticalIncidentCount, openIncidentCount)
56
+ },
57
+ {
58
+ label: "Avg latency",
59
+ value: average(
60
+ services
61
+ .map((service) => service.latencyMs)
62
+ .filter((value): value is number => value !== undefined)
63
+ ),
64
+ trend: serviceCount ? `${serviceCount} monitored` : undefined
65
+ }
66
+ ];
67
+
68
+ return (
69
+ <Surface
70
+ {...props}
71
+ title={title ?? "System health"}
72
+ description={
73
+ description ??
74
+ healthSummary(serviceCount, healthyCount, degradedCount, downCount, openIncidentCount)
75
+ }
76
+ severity={severity ?? defaultSeverity}
77
+ status={status ?? defaultStatus}
78
+ className={["eth-admin-system-health", className].filter(Boolean).join(" ")}
79
+ data-eth-component="SystemHealthDashboard"
80
+ >
81
+ <div className="eth-admin-system-health__metrics">
82
+ {computedMetrics.map((metric) => (
83
+ <KPIBlock
84
+ key={String(metric.label)}
85
+ title={metric.label}
86
+ value={metric.value}
87
+ status={metric.trend}
88
+ />
89
+ ))}
90
+ </div>
91
+ <div className="eth-admin-system-health__services">
92
+ {services.length ? (
93
+ services.map((service) => {
94
+ const serviceIncidents = incidents.filter((incident) =>
95
+ incident.title.toLowerCase().includes(service.name.toLowerCase())
96
+ );
97
+
98
+ return (
99
+ <ServiceStatusCard
100
+ key={service.id}
101
+ service={{
102
+ name: service.name,
103
+ status: service.status,
104
+ latencyMs: service.latencyMs,
105
+ uptimePercent: service.status === "healthy" ? 99.9 : undefined,
106
+ incidentsCount: service.incidentsCount ?? serviceIncidents.length,
107
+ lastIncidentAt: serviceIncidents[0]?.startedAt
108
+ }}
109
+ />
110
+ );
111
+ })
112
+ ) : (
113
+ <p className="eth-admin-system-health__empty">No services are currently registered.</p>
114
+ )}
115
+ </div>
116
+ {incidents.length ? (
117
+ <section className="eth-admin-system-health__incidents">
118
+ <h3>Incidents</h3>
119
+ <ul>
120
+ {incidents.map((incident) => (
121
+ <li key={incident.id}>
122
+ <Badge severity={severityForIncident(incident.severity)}>{incident.severity}</Badge>
123
+ <span>{incident.title}</span>
124
+ <time>{incident.startedAt}</time>
125
+ </li>
126
+ ))}
127
+ </ul>
128
+ </section>
129
+ ) : null}
130
+ </Surface>
131
+ );
132
+ }
133
+
134
+ function average(values: number[]) {
135
+ if (!values.length) return "0 ms";
136
+ return `${Math.round(values.reduce((sum, value) => sum + value, 0) / values.length)} ms`;
137
+ }
138
+
139
+ function healthSummary(
140
+ serviceCount: number,
141
+ healthyCount: number,
142
+ degradedCount: number,
143
+ downCount: number,
144
+ openIncidentCount: number
145
+ ) {
146
+ if (!serviceCount) return "No services registered for this tenant.";
147
+
148
+ const exceptions: string[] = [];
149
+ if (degradedCount) exceptions.push(`${degradedCount} degraded`);
150
+ if (downCount) exceptions.push(`${downCount} down`);
151
+ if (openIncidentCount) {
152
+ exceptions.push(`${openIncidentCount} open ${pluralize(openIncidentCount, "incident")}`);
153
+ }
154
+
155
+ const serviceSummary = `${healthyCount} of ${serviceCount} ${pluralize(serviceCount, "service")} healthy`;
156
+ return exceptions.length
157
+ ? `${serviceSummary}; ${exceptions.join(", ")}.`
158
+ : `${serviceSummary}; no open incidents.`;
159
+ }
160
+
161
+ function serviceHealthTrend(healthyCount: number, degradedCount: number, downCount: number) {
162
+ if (downCount) {
163
+ return <Badge severity="danger">{`${downCount} down`}</Badge>;
164
+ }
165
+ if (degradedCount) {
166
+ return <Badge severity="warning">{`${degradedCount} degraded`}</Badge>;
167
+ }
168
+ if (healthyCount) {
169
+ return <Badge severity="success">Nominal</Badge>;
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ function incidentTrend(criticalIncidentCount: number, openIncidentCount: number) {
175
+ if (criticalIncidentCount) {
176
+ return <Badge severity="danger">{`${criticalIncidentCount} critical`}</Badge>;
177
+ }
178
+ if (openIncidentCount) {
179
+ return <Badge severity="warning">Investigating</Badge>;
180
+ }
181
+ return <Badge severity="success">Clear</Badge>;
182
+ }
183
+
184
+ function severityForHealth(
185
+ downCount: number,
186
+ degradedCount: number,
187
+ criticalIncidentCount: number,
188
+ openIncidentCount: number
189
+ ): EthSeverity | undefined {
190
+ if (downCount || criticalIncidentCount) return "danger";
191
+ if (degradedCount || openIncidentCount) return "warning";
192
+ return undefined;
193
+ }
194
+
195
+ function statusForHealth(
196
+ serviceCount: number,
197
+ downCount: number,
198
+ degradedCount: number
199
+ ): EthOperationalStatus {
200
+ if (!serviceCount) return "inactive";
201
+ if (downCount) return "failed";
202
+ if (degradedCount) return "warning";
203
+ return "active";
204
+ }
205
+
206
+ function pluralize(count: number, singular: string) {
207
+ return count === 1 ? singular : `${singular}s`;
208
+ }
209
+
210
+ function severityForIncident(severity: AdminIncident["severity"]): EthSeverity {
211
+ if (severity === "critical" || severity === "error") return "danger";
212
+ if (severity === "warning") return "warning";
213
+ return "info";
214
+ }
@@ -0,0 +1,38 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { TenantSettingsPanel } from "./TenantSettingsPanel";
4
+
5
+ describe("TenantSettingsPanel", () => {
6
+ it("renders tenant settings with an admin styling hook and explicit save action", () => {
7
+ const { container } = render(
8
+ <TenantSettingsPanel
9
+ submitLabel="Save settings"
10
+ values={{ tenantName: "EchoThink, Inc.", region: "us-west" }}
11
+ sections={[
12
+ {
13
+ id: "general",
14
+ title: "General",
15
+ description: "Tenant identity and default region.",
16
+ fields: [
17
+ { name: "tenantName", type: "text", label: "Tenant name", required: true },
18
+ {
19
+ name: "region",
20
+ type: "select",
21
+ label: "Region",
22
+ options: [
23
+ { value: "us-west", label: "US West" },
24
+ { value: "eu-west", label: "EU West" }
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ ]}
30
+ />
31
+ );
32
+
33
+ expect(container.querySelector(".eth-admin-tenant-settings")).toBeTruthy();
34
+ expect(screen.getByRole("heading", { name: "Tenant settings" })).toBeTruthy();
35
+ expect(screen.getByText("Tenant identity and default region.")).toBeTruthy();
36
+ expect(screen.getByRole("button", { name: "Save settings" })).toBeTruthy();
37
+ });
38
+ });
@@ -0,0 +1,44 @@
1
+ import { ConfigForm, type FormValues } from "@echothink-ui/forms";
2
+ import type { SurfaceComponentProps } from "@echothink-ui/core";
3
+ import type { TenantSettingsSection } from "./types";
4
+
5
+ export interface TenantSettingsPanelProps
6
+ extends Omit<SurfaceComponentProps, "children" | "onChange" | "onSubmit"> {
7
+ sections: TenantSettingsSection[];
8
+ values: FormValues;
9
+ onChange?: (values: FormValues) => void;
10
+ onSubmit?: (values: FormValues) => void;
11
+ submitLabel?: string;
12
+ }
13
+
14
+ export function TenantSettingsPanel({
15
+ sections,
16
+ values,
17
+ onChange,
18
+ onSubmit,
19
+ submitLabel = "Save settings",
20
+ title,
21
+ description,
22
+ className,
23
+ ...props
24
+ }: TenantSettingsPanelProps) {
25
+ const effectiveDescription =
26
+ description === undefined
27
+ ? "Manage tenant identity, regional placement, and governance defaults."
28
+ : description;
29
+
30
+ return (
31
+ <ConfigForm
32
+ {...props}
33
+ title={title ?? "Tenant settings"}
34
+ description={effectiveDescription}
35
+ className={["eth-admin-tenant-settings", className].filter(Boolean).join(" ")}
36
+ sections={sections}
37
+ values={values}
38
+ onChange={onChange}
39
+ onSubmit={onSubmit}
40
+ submitLabel={submitLabel}
41
+ data-eth-component="TenantSettingsPanel"
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,32 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { WorkerPoolPanel } from "./WorkerPoolPanel";
4
+
5
+ describe("WorkerPoolPanel", () => {
6
+ it("renders worker capacity, health summary, and scale controls", () => {
7
+ const onScale = vi.fn();
8
+
9
+ render(
10
+ <WorkerPoolPanel
11
+ pools={[
12
+ { id: "render", name: "render", active: 12, total: 16, idle: 4, failed: 0 },
13
+ { id: "send", name: "send", active: 5, total: 8, idle: 3, failed: 1 }
14
+ ]}
15
+ onScale={onScale}
16
+ />
17
+ );
18
+
19
+ expect(screen.getByRole("heading", { name: "Worker pools" })).toBeTruthy();
20
+ expect(screen.getByText("2 pools")).toBeTruthy();
21
+ expect(screen.getByText("17 / 24")).toBeTruthy();
22
+ expect(screen.getByText("1 failed")).toBeTruthy();
23
+ expect(screen.getByText("Failure in pool")).toBeTruthy();
24
+
25
+ const capacity = screen.getByRole("progressbar", { name: "render capacity" });
26
+ expect(capacity.getAttribute("aria-valuenow")).toBe("75");
27
+ expect(capacity.getAttribute("aria-valuetext")).toBe("12 of 16 workers active");
28
+
29
+ fireEvent.click(screen.getByRole("button", { name: "Scale render up by 1" }));
30
+ expect(onScale).toHaveBeenCalledWith("render", 1);
31
+ });
32
+ });