@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,171 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActionGroup,
4
+ Badge,
5
+ type EthAction,
6
+ type EthSeverity,
7
+ type SurfaceComponentProps
8
+ } from "@echothink-ui/core";
9
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
10
+ import type { JobQueue } from "./types";
11
+
12
+ export interface JobQueuePanelProps extends Omit<SurfaceComponentProps, "children" | "actions"> {
13
+ queues: JobQueue[];
14
+ actions?: EthAction[];
15
+ }
16
+
17
+ type JobQueueRow = Record<string, unknown> & JobQueue;
18
+
19
+ export function JobQueuePanel({
20
+ queues,
21
+ actions,
22
+ title = "Job queues",
23
+ subtitle,
24
+ description,
25
+ density = "compact",
26
+ className,
27
+ "aria-labelledby": ariaLabelledBy,
28
+ eyebrow: _eyebrow,
29
+ status: _status,
30
+ severity: _severity,
31
+ loading: _loading,
32
+ empty: _empty,
33
+ error: _error,
34
+ items: _items,
35
+ metadata: _metadata,
36
+ footer: _footer,
37
+ ...props
38
+ }: JobQueuePanelProps) {
39
+ const headingId = React.useId();
40
+ const rows: JobQueueRow[] = queues.map((queue) => ({ ...queue }));
41
+ const subtitleContent = subtitle ?? description;
42
+ const totals = queues.reduce(
43
+ (summary, queue) => ({
44
+ depth: summary.depth + queue.depth,
45
+ processing: summary.processing + queue.processing,
46
+ failed: summary.failed + queue.failed
47
+ }),
48
+ { depth: 0, processing: 0, failed: 0 }
49
+ );
50
+ const columns: DataColumn<JobQueueRow>[] = [
51
+ {
52
+ key: "name",
53
+ header: "Queue",
54
+ width: "34%",
55
+ render: (row) => (
56
+ <div className="eth-admin-job-queue__queue">
57
+ <strong>{row.name}</strong>
58
+ <span>{queueStateLabel(row)}</span>
59
+ </div>
60
+ )
61
+ },
62
+ {
63
+ key: "depth",
64
+ header: "Depth",
65
+ align: "end",
66
+ width: "16%",
67
+ render: (row) => <span className="eth-admin-job-queue__number">{formatCount(row.depth)}</span>
68
+ },
69
+ {
70
+ key: "processing",
71
+ header: "Processing",
72
+ align: "end",
73
+ width: "18%",
74
+ render: (row) => (
75
+ <span className="eth-admin-job-queue__number">{formatCount(row.processing)}</span>
76
+ )
77
+ },
78
+ {
79
+ key: "failed",
80
+ header: "Failed",
81
+ align: "end",
82
+ width: "14%",
83
+ render: (row) => (
84
+ <Badge severity={row.failed > 0 ? "danger" : "success"}>{formatCount(row.failed)}</Badge>
85
+ )
86
+ },
87
+ {
88
+ key: "retryRate",
89
+ header: "Retry rate",
90
+ align: "end",
91
+ width: "18%",
92
+ render: (row) =>
93
+ row.retryRate === undefined ? (
94
+ <span className="eth-admin-job-queue__muted">n/a</span>
95
+ ) : (
96
+ <Badge severity={retryRateSeverity(row.retryRate)}>
97
+ {formatRetryRate(row.retryRate)}
98
+ </Badge>
99
+ )
100
+ }
101
+ ];
102
+ const statusSeverity: EthSeverity =
103
+ queues.length === 0
104
+ ? "neutral"
105
+ : totals.failed > 0
106
+ ? "danger"
107
+ : totals.processing > 0
108
+ ? "info"
109
+ : "success";
110
+ const statusLabel =
111
+ queues.length === 0 ? "No queues" : totals.failed > 0 ? "Action needed" : "Operational";
112
+
113
+ return (
114
+ <section
115
+ {...props}
116
+ className={["eth-admin-job-queue", className].filter(Boolean).join(" ")}
117
+ data-eth-component="JobQueuePanel"
118
+ aria-labelledby={ariaLabelledBy ?? headingId}
119
+ >
120
+ <header>
121
+ <div className="eth-admin-job-queue__heading">
122
+ <h3 id={headingId}>{title}</h3>
123
+ {subtitleContent ? (
124
+ <div className="eth-admin-job-queue__subtitle">{subtitleContent}</div>
125
+ ) : null}
126
+ </div>
127
+ <div className="eth-admin-job-queue__header-actions">
128
+ <Badge severity={statusSeverity}>{statusLabel}</Badge>
129
+ <ActionGroup actions={actions} />
130
+ </div>
131
+ </header>
132
+ <dl className="eth-admin-job-queue__summary" aria-label="Queue totals">
133
+ <div>
134
+ <dt>Total depth</dt>
135
+ <dd>{formatCount(totals.depth)}</dd>
136
+ </div>
137
+ <div>
138
+ <dt>Processing</dt>
139
+ <dd>{formatCount(totals.processing)}</dd>
140
+ </div>
141
+ <div>
142
+ <dt>Failed</dt>
143
+ <dd>{formatCount(totals.failed)}</dd>
144
+ </div>
145
+ </dl>
146
+ <DataTable rows={rows} columns={columns} density={density} />
147
+ </section>
148
+ );
149
+ }
150
+
151
+ function queueStateLabel(queue: JobQueue) {
152
+ if (queue.failed > 0) return "Failure requires review";
153
+ if (queue.processing > 0) return "Workers active";
154
+ if (queue.depth > 0) return "Waiting for capacity";
155
+ return "Idle";
156
+ }
157
+
158
+ function retryRateSeverity(retryRate: number): EthSeverity {
159
+ if (retryRate >= 1) return "danger";
160
+ if (retryRate > 0.1) return "warning";
161
+ if (retryRate > 0) return "info";
162
+ return "success";
163
+ }
164
+
165
+ function formatCount(value: number) {
166
+ return new Intl.NumberFormat("en-US").format(value);
167
+ }
168
+
169
+ function formatRetryRate(value: number) {
170
+ return `${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
171
+ }
@@ -0,0 +1,45 @@
1
+ import { render, screen, within } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { PolicyConfigPanel } from "./PolicyConfigPanel";
4
+
5
+ describe("PolicyConfigPanel", () => {
6
+ it("renders policy metadata, scope summary, and an enabled save action", () => {
7
+ render(
8
+ <PolicyConfigPanel
9
+ policy={{
10
+ id: "external-send-review",
11
+ name: "External send review",
12
+ description: "External messages require legal review before delivery.",
13
+ variables: ["recipient_domain", "message_channel", "risk_score"],
14
+ rules: [
15
+ {
16
+ id: "external-domain",
17
+ when: { kind: "leaf", field: "recipient_domain", op: "eq", value: "external" },
18
+ then: { type: "require-approval", target: "legal-review" }
19
+ }
20
+ ]
21
+ }}
22
+ schema={{
23
+ description: "Approval gates for outbound communications.",
24
+ properties: {
25
+ recipient_domain: { type: "string", title: "Recipient domain" },
26
+ message_channel: { type: "string", title: "Message channel" },
27
+ risk_score: { type: "number", title: "Risk score" }
28
+ }
29
+ }}
30
+ onChange={() => undefined}
31
+ onSave={() => undefined}
32
+ />
33
+ );
34
+
35
+ expect(screen.getByRole("heading", { name: "Policy configuration" })).toBeTruthy();
36
+
37
+ const summary = screen.getByRole("complementary", { name: "Policy summary" });
38
+ expect(within(summary).getByText("Configured")).toBeTruthy();
39
+ expect(within(summary).getByText("recipient_domain")).toBeTruthy();
40
+ expect(within(summary).getByText("message_channel")).toBeTruthy();
41
+
42
+ const saveButton = screen.getByRole("button", { name: "Save policy" });
43
+ expect(saveButton.hasAttribute("disabled")).toBe(false);
44
+ });
45
+ });
@@ -0,0 +1,131 @@
1
+ import { useId } from "react";
2
+ import {
3
+ Badge,
4
+ Button,
5
+ FormField,
6
+ Surface,
7
+ Tag,
8
+ TextInput,
9
+ Textarea,
10
+ type SurfaceComponentProps
11
+ } from "@echothink-ui/core";
12
+ import { RuleBuilder } from "@echothink-ui/forms";
13
+ import type { AdminPolicyConfig, JsonSchema } from "./types";
14
+
15
+ export interface PolicyConfigPanelProps
16
+ extends Omit<SurfaceComponentProps, "children" | "onChange"> {
17
+ policy: AdminPolicyConfig;
18
+ schema: JsonSchema;
19
+ onChange?: (policy: AdminPolicyConfig) => void;
20
+ onSave?: (policy: AdminPolicyConfig) => void;
21
+ }
22
+
23
+ export function PolicyConfigPanel({
24
+ policy,
25
+ schema,
26
+ onChange,
27
+ onSave,
28
+ title,
29
+ description,
30
+ density = "default",
31
+ className,
32
+ ...props
33
+ }: PolicyConfigPanelProps) {
34
+ const generatedId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
35
+ const policyDomId = `eth-admin-policy-${String(policy.id ?? generatedId).replace(
36
+ /[^a-zA-Z0-9_-]/g,
37
+ "-"
38
+ )}`;
39
+ const update = (patch: Partial<AdminPolicyConfig>) => onChange?.({ ...policy, ...patch });
40
+ const schemaFields = Object.keys(schema.properties ?? {});
41
+ const variables = policy.variables?.length ? policy.variables : schemaFields;
42
+ const rules = policy.rules ?? [];
43
+ const visibleVariables = variables.slice(0, 5);
44
+ const hiddenVariableCount = Math.max(0, variables.length - visibleVariables.length);
45
+
46
+ return (
47
+ <Surface
48
+ {...props}
49
+ title={title ?? "Policy configuration"}
50
+ description={
51
+ description ??
52
+ schema.description ??
53
+ "Configure policy metadata and conditional rules for governed operations."
54
+ }
55
+ density={density}
56
+ className={["eth-admin-policy-config", className].filter(Boolean).join(" ")}
57
+ data-eth-component="PolicyConfigPanel"
58
+ >
59
+ <div className="eth-admin-policy-config__layout">
60
+ <div className="eth-admin-policy-config__fields">
61
+ <FormField id={`${policyDomId}-name`} label="Policy name" required>
62
+ <TextInput
63
+ id={`${policyDomId}-name`}
64
+ density={density}
65
+ value={policy.name ?? ""}
66
+ onChange={(event) => update({ name: event.currentTarget.value })}
67
+ />
68
+ </FormField>
69
+ <FormField id={`${policyDomId}-description`} label="Description">
70
+ <Textarea
71
+ id={`${policyDomId}-description`}
72
+ rows={4}
73
+ value={policy.description ?? ""}
74
+ onChange={(event) => update({ description: event.currentTarget.value })}
75
+ />
76
+ </FormField>
77
+ </div>
78
+
79
+ <aside className="eth-admin-policy-config__summary" aria-label="Policy summary">
80
+ <div className="eth-admin-policy-config__summary-header">
81
+ <h3>Policy scope</h3>
82
+ <Badge severity={rules.length ? "success" : "warning"}>
83
+ {rules.length ? "Configured" : "Needs rules"}
84
+ </Badge>
85
+ </div>
86
+ <dl className="eth-admin-policy-config__summary-grid">
87
+ <div>
88
+ <dt>Rules</dt>
89
+ <dd>{rules.length}</dd>
90
+ </div>
91
+ <div>
92
+ <dt>Variables</dt>
93
+ <dd>{variables.length}</dd>
94
+ </div>
95
+ <div>
96
+ <dt>Schema fields</dt>
97
+ <dd>{schemaFields.length}</dd>
98
+ </div>
99
+ </dl>
100
+ <div className="eth-admin-policy-config__variables">
101
+ <span>Rule variables</span>
102
+ {variables.length ? (
103
+ <div className="eth-admin-policy-config__variable-list">
104
+ {visibleVariables.map((variable) => (
105
+ <Tag key={variable}>{variable}</Tag>
106
+ ))}
107
+ {hiddenVariableCount ? (
108
+ <Badge severity="neutral">+{hiddenVariableCount} more</Badge>
109
+ ) : null}
110
+ </div>
111
+ ) : (
112
+ <p>No variables available.</p>
113
+ )}
114
+ </div>
115
+ </aside>
116
+ </div>
117
+
118
+ <RuleBuilder
119
+ className="eth-admin-policy-config__rules"
120
+ rules={rules}
121
+ variables={variables}
122
+ onChange={(nextRules) => update({ rules: nextRules })}
123
+ />
124
+ <div className="eth-admin-policy-config__actions">
125
+ <Button type="button" disabled={!onSave} onClick={() => onSave?.(policy)}>
126
+ Save policy
127
+ </Button>
128
+ </div>
129
+ </Surface>
130
+ );
131
+ }
@@ -0,0 +1,70 @@
1
+ import { ChartBlock } from "@echothink-ui/charts";
2
+ import type { SurfaceComponentProps } from "@echothink-ui/core";
3
+ import type { QueueDepthPoint } from "./types";
4
+
5
+ export interface QueueDepthChartProps extends Omit<SurfaceComponentProps, "children"> {
6
+ points: QueueDepthPoint[];
7
+ }
8
+
9
+ const depthFormatter = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
10
+ const queueDepthSeries = [{ key: "depth", label: "Queue depth", color: "#0f62fe" }];
11
+
12
+ export function QueueDepthChart({
13
+ points,
14
+ title,
15
+ metadata,
16
+ className,
17
+ ...props
18
+ }: QueueDepthChartProps) {
19
+ const data = points.map((point, index) => {
20
+ const row = {
21
+ label: point.label ?? point.timestamp ?? String(index + 1),
22
+ depth: point.depth
23
+ };
24
+
25
+ return point.timestamp ? { ...row, timestamp: point.timestamp } : row;
26
+ });
27
+
28
+ return (
29
+ <ChartBlock
30
+ {...props}
31
+ className={["eth-admin-queue-depth-chart", className].filter(Boolean).join(" ")}
32
+ title={title ?? "Queue depth"}
33
+ metadata={metadata ?? queueDepthMetadata(points)}
34
+ chartType="line"
35
+ data={data}
36
+ valueKey="depth"
37
+ series={queueDepthSeries}
38
+ data-eth-component="QueueDepthChart"
39
+ />
40
+ );
41
+ }
42
+
43
+ function queueDepthMetadata(points: QueueDepthPoint[]): SurfaceComponentProps["metadata"] {
44
+ const depths = points.map((point) => point.depth).filter(Number.isFinite);
45
+ if (!depths.length) return [];
46
+
47
+ const current = depths.at(-1) ?? 0;
48
+ const previous = depths.at(-2);
49
+ const peak = Math.max(...depths);
50
+ const average = depths.reduce((total, depth) => total + depth, 0) / depths.length;
51
+
52
+ return [
53
+ { label: "Current depth", value: formatDepth(current) },
54
+ { label: "Peak", value: formatDepth(peak) },
55
+ { label: "Average", value: formatDepth(average) },
56
+ {
57
+ label: "Latest change",
58
+ value: previous === undefined ? "n/a" : formatDelta(current - previous)
59
+ }
60
+ ];
61
+ }
62
+
63
+ function formatDepth(value: number) {
64
+ return depthFormatter.format(value);
65
+ }
66
+
67
+ function formatDelta(value: number) {
68
+ if (value === 0) return "No change";
69
+ return `${value > 0 ? "+" : ""}${formatDepth(value)}`;
70
+ }
@@ -0,0 +1,29 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { RateLimitPanel } from "./RateLimitPanel";
4
+
5
+ describe("RateLimitPanel", () => {
6
+ it("renders rate-limit usage, summary state, and non-editable configuration without an update handler", () => {
7
+ render(
8
+ <RateLimitPanel
9
+ limits={[
10
+ { id: "send", name: "send.email", current: 80, limit: 100, windowSeconds: 60 },
11
+ { id: "ingest", name: "ingest.docs", current: 200, limit: 1000, windowSeconds: 60 }
12
+ ]}
13
+ />
14
+ );
15
+
16
+ expect(screen.getByRole("heading", { name: "Rate limits" })).toBeTruthy();
17
+ expect(screen.getByText("1 near limit")).toBeTruthy();
18
+ expect(screen.getByText("send.email 80%")).toBeTruthy();
19
+
20
+ const usage = screen.getByRole("progressbar", { name: "send.email usage" });
21
+ expect(usage.getAttribute("aria-valuenow")).toBe("80");
22
+ expect(usage.getAttribute("aria-valuetext")).toBe("80% used, 80 of 100 requests");
23
+
24
+ const configuredLimit = screen.getByRole("spinbutton", {
25
+ name: "send.email configured limit"
26
+ });
27
+ expect(configuredLimit.hasAttribute("readonly")).toBe(true);
28
+ });
29
+ });
@@ -0,0 +1,249 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActionGroup,
4
+ Badge,
5
+ NumberInput,
6
+ type EthSeverity,
7
+ type SurfaceComponentProps
8
+ } from "@echothink-ui/core";
9
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
10
+ import type { RateLimit } from "./types";
11
+
12
+ export interface RateLimitPanelProps extends Omit<SurfaceComponentProps, "children"> {
13
+ limits: RateLimit[];
14
+ onUpdate?: (id: string, update: Partial<RateLimit>) => void;
15
+ }
16
+
17
+ type RateLimitTone = "normal" | "warning" | "danger" | "neutral";
18
+
19
+ interface RateLimitState {
20
+ label: string;
21
+ severity: EthSeverity;
22
+ tone: RateLimitTone;
23
+ }
24
+
25
+ type RateLimitMeterStyle = React.CSSProperties & {
26
+ "--eth-admin-rate-limit-usage": string;
27
+ };
28
+
29
+ type RateLimitRow = Record<string, unknown> &
30
+ RateLimit & {
31
+ cappedPercent: number;
32
+ percent: number;
33
+ percentLabel: string;
34
+ state: RateLimitState;
35
+ usageLabel: string;
36
+ };
37
+
38
+ export function RateLimitPanel({
39
+ limits,
40
+ onUpdate,
41
+ title = "Rate limits",
42
+ subtitle,
43
+ description,
44
+ actions,
45
+ density = "compact",
46
+ className,
47
+ "aria-label": ariaLabel,
48
+ "aria-labelledby": ariaLabelledBy,
49
+ eyebrow: _eyebrow,
50
+ status: _status,
51
+ severity: _severity,
52
+ loading: _loading,
53
+ empty: _empty,
54
+ error: _error,
55
+ items: _items,
56
+ metadata: _metadata,
57
+ footer: _footer,
58
+ ...sectionProps
59
+ }: RateLimitPanelProps) {
60
+ const headingId = React.useId();
61
+ const rows: RateLimitRow[] = limits.map((limit) => {
62
+ const percent = limit.limit > 0 ? (limit.current / limit.limit) * 100 : 0;
63
+ const cappedPercent = Math.min(100, Math.max(0, percent));
64
+
65
+ return {
66
+ ...limit,
67
+ cappedPercent,
68
+ percent,
69
+ percentLabel: limit.limit > 0 ? formatPercent(percent) : "No limit",
70
+ state: rateLimitState(percent, limit.limit),
71
+ usageLabel:
72
+ limit.limit > 0
73
+ ? `${formatNumber(limit.current)} / ${formatNumber(limit.limit)}`
74
+ : `${formatNumber(limit.current)} requests`
75
+ };
76
+ });
77
+ const highestUsage = rows.reduce<RateLimitRow | undefined>((highest, row) => {
78
+ if (row.limit <= 0) return highest;
79
+ if (!highest || row.percent > highest.percent) return row;
80
+ return highest;
81
+ }, undefined);
82
+ const exceededCount = rows.filter((row) => row.limit > 0 && row.percent >= 100).length;
83
+ const nearLimitCount = rows.filter(
84
+ (row) => row.limit > 0 && row.percent >= 80 && row.percent < 100
85
+ ).length;
86
+ const statusSummary = rateLimitStatusSummary(limits.length, exceededCount, nearLimitCount);
87
+ const supportingText = subtitle ?? description;
88
+ const columns: DataColumn<RateLimitRow>[] = [
89
+ {
90
+ key: "name",
91
+ header: "Limit",
92
+ width: "28%",
93
+ render: (row) => (
94
+ <div className="eth-admin-rate-limit__limit">
95
+ <strong>{row.name}</strong>
96
+ <span>{row.state.label}</span>
97
+ </div>
98
+ )
99
+ },
100
+ {
101
+ key: "current",
102
+ header: "Usage",
103
+ width: "34%",
104
+ render: (row) => (
105
+ <div className="eth-admin-rate-limit__usage">
106
+ <div className="eth-admin-rate-limit__usage-copy">
107
+ <span>{row.usageLabel}</span>
108
+ <strong>{row.percentLabel}</strong>
109
+ </div>
110
+ <span
111
+ className="eth-admin-rate-limit__meter"
112
+ role="progressbar"
113
+ aria-label={`${row.name} usage`}
114
+ aria-valuemin={0}
115
+ aria-valuemax={100}
116
+ aria-valuenow={Math.round(row.cappedPercent)}
117
+ aria-valuetext={
118
+ row.limit > 0
119
+ ? `${formatPercent(row.percent)} used, ${formatNumber(row.current)} of ${formatNumber(
120
+ row.limit
121
+ )} requests`
122
+ : "No configured limit"
123
+ }
124
+ style={
125
+ {
126
+ "--eth-admin-rate-limit-usage": `${row.cappedPercent}%`
127
+ } as RateLimitMeterStyle
128
+ }
129
+ >
130
+ <span
131
+ className={[
132
+ "eth-admin-rate-limit__meter-fill",
133
+ `eth-admin-rate-limit__meter-fill--${row.state.tone}`
134
+ ].join(" ")}
135
+ />
136
+ </span>
137
+ </div>
138
+ )
139
+ },
140
+ {
141
+ key: "windowSeconds",
142
+ header: "Window",
143
+ width: "8rem",
144
+ render: (row) => (
145
+ <span className="eth-admin-rate-limit__number">{formatWindow(row.windowSeconds)}</span>
146
+ )
147
+ },
148
+ {
149
+ key: "limit",
150
+ header: "Configured limit",
151
+ width: "13rem",
152
+ render: (row) => (
153
+ <NumberInput
154
+ aria-label={`${row.name} configured limit`}
155
+ className="eth-admin-rate-limit__limit-input"
156
+ density={density}
157
+ hideLabel
158
+ min={0}
159
+ step={1}
160
+ readOnly={!onUpdate}
161
+ value={row.limit}
162
+ onChange={(event) => {
163
+ const nextLimit = Number(event.currentTarget.value);
164
+ if (Number.isFinite(nextLimit)) onUpdate?.(row.id, { limit: nextLimit });
165
+ }}
166
+ />
167
+ )
168
+ }
169
+ ];
170
+
171
+ return (
172
+ <section
173
+ {...sectionProps}
174
+ aria-label={ariaLabel}
175
+ aria-labelledby={ariaLabelledBy ?? (ariaLabel ? undefined : headingId)}
176
+ className={["eth-admin-rate-limit", className].filter(Boolean).join(" ")}
177
+ data-eth-component="RateLimitPanel"
178
+ >
179
+ <header>
180
+ <div className="eth-admin-rate-limit__heading">
181
+ <h3 id={headingId}>{title}</h3>
182
+ {supportingText ? <p>{supportingText}</p> : null}
183
+ </div>
184
+ <div className="eth-admin-rate-limit__header-actions">
185
+ <Badge severity={statusSummary.severity}>{statusSummary.label}</Badge>
186
+ <ActionGroup actions={actions} />
187
+ </div>
188
+ </header>
189
+ <dl className="eth-admin-rate-limit__summary" aria-label="Rate limit totals">
190
+ <div>
191
+ <dt>Tracked limits</dt>
192
+ <dd>{formatNumber(limits.length)}</dd>
193
+ </div>
194
+ <div>
195
+ <dt>Highest usage</dt>
196
+ <dd>
197
+ {highestUsage ? `${highestUsage.name} ${formatPercent(highestUsage.percent)}` : "n/a"}
198
+ </dd>
199
+ </div>
200
+ <div>
201
+ <dt>Needs review</dt>
202
+ <dd>{formatNumber(exceededCount + nearLimitCount)}</dd>
203
+ </div>
204
+ </dl>
205
+ <DataTable
206
+ rows={rows}
207
+ columns={columns}
208
+ rowKey="id"
209
+ density={density}
210
+ className="eth-admin-rate-limit__table"
211
+ emptyState={<p className="eth-admin-rate-limit__empty">No rate limits configured.</p>}
212
+ />
213
+ </section>
214
+ );
215
+ }
216
+
217
+ function rateLimitState(percent: number, limit: number): RateLimitState {
218
+ if (limit <= 0) return { label: "No limit", severity: "neutral", tone: "neutral" };
219
+ if (percent >= 100) return { label: "Exceeded", severity: "danger", tone: "danger" };
220
+ if (percent >= 80) return { label: "Near limit", severity: "warning", tone: "warning" };
221
+ return { label: "Within limit", severity: "success", tone: "normal" };
222
+ }
223
+
224
+ function rateLimitStatusSummary(
225
+ totalCount: number,
226
+ exceededCount: number,
227
+ nearLimitCount: number
228
+ ): Pick<RateLimitState, "label" | "severity"> {
229
+ if (totalCount === 0) return { label: "No limits", severity: "neutral" };
230
+ if (exceededCount > 0) {
231
+ return { label: `${formatNumber(exceededCount)} exceeded`, severity: "danger" };
232
+ }
233
+ if (nearLimitCount > 0) {
234
+ return { label: `${formatNumber(nearLimitCount)} near limit`, severity: "warning" };
235
+ }
236
+ return { label: "Within limits", severity: "success" };
237
+ }
238
+
239
+ function formatWindow(seconds: number) {
240
+ return `${formatNumber(seconds)}s`;
241
+ }
242
+
243
+ function formatPercent(value: number) {
244
+ return `${Math.round(value).toLocaleString("en-US")}%`;
245
+ }
246
+
247
+ function formatNumber(value: number) {
248
+ return new Intl.NumberFormat("en-US").format(value);
249
+ }