@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,281 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActionGroup,
4
+ Badge,
5
+ Button,
6
+ type EthSeverity,
7
+ type SurfaceComponentProps
8
+ } from "@echothink-ui/core";
9
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
10
+ import type { WorkerPool } from "./types";
11
+
12
+ export interface WorkerPoolPanelProps extends Omit<SurfaceComponentProps, "children"> {
13
+ pools: WorkerPool[];
14
+ onScale?: (id: string, delta: number) => void;
15
+ }
16
+
17
+ type WorkerPoolTone = "normal" | "warning" | "danger" | "neutral";
18
+
19
+ interface WorkerPoolState {
20
+ label: string;
21
+ tone: WorkerPoolTone;
22
+ }
23
+
24
+ interface WorkerPoolStatusSummary {
25
+ label: string;
26
+ severity: EthSeverity;
27
+ }
28
+
29
+ type WorkerPoolMeterStyle = React.CSSProperties & {
30
+ "--eth-admin-worker-pool-capacity": string;
31
+ };
32
+
33
+ type WorkerPoolRow = Record<string, unknown> &
34
+ WorkerPool & {
35
+ cappedPercent: number;
36
+ capacityLabel: string;
37
+ percent: number;
38
+ percentLabel: string;
39
+ state: WorkerPoolState;
40
+ };
41
+
42
+ export function WorkerPoolPanel({
43
+ pools,
44
+ onScale,
45
+ title = "Worker pools",
46
+ subtitle,
47
+ description,
48
+ actions,
49
+ density = "compact",
50
+ className,
51
+ "aria-label": ariaLabel,
52
+ "aria-labelledby": ariaLabelledBy,
53
+ eyebrow: _eyebrow,
54
+ status: _status,
55
+ severity: _severity,
56
+ loading: _loading,
57
+ empty: _empty,
58
+ error: _error,
59
+ items: _items,
60
+ metadata: _metadata,
61
+ footer: _footer,
62
+ ...sectionProps
63
+ }: WorkerPoolPanelProps) {
64
+ const headingId = React.useId();
65
+ const rows: WorkerPoolRow[] = pools.map((pool) => {
66
+ const percent = pool.total > 0 ? (pool.active / pool.total) * 100 : 0;
67
+ const cappedPercent = Math.min(100, Math.max(0, percent));
68
+
69
+ return {
70
+ ...pool,
71
+ cappedPercent,
72
+ capacityLabel:
73
+ pool.total > 0
74
+ ? `${formatNumber(pool.active)} / ${formatNumber(pool.total)} active`
75
+ : `${formatNumber(pool.active)} active`,
76
+ percent,
77
+ percentLabel: pool.total > 0 ? formatPercent(percent) : "No capacity",
78
+ state: workerPoolState(pool, percent)
79
+ };
80
+ });
81
+ const totals = pools.reduce(
82
+ (summary, pool) => ({
83
+ active: summary.active + pool.active,
84
+ failed: summary.failed + pool.failed,
85
+ idle: summary.idle + pool.idle,
86
+ total: summary.total + pool.total
87
+ }),
88
+ { active: 0, failed: 0, idle: 0, total: 0 }
89
+ );
90
+ const utilization = totals.total > 0 ? (totals.active / totals.total) * 100 : 0;
91
+ const statusSummary = workerPoolStatusSummary(pools.length, totals.failed, utilization);
92
+ const supportingText = subtitle ?? description;
93
+ const columns: DataColumn<WorkerPoolRow>[] = [
94
+ {
95
+ key: "name",
96
+ header: "Pool",
97
+ width: "26%",
98
+ render: (row) => (
99
+ <div className="eth-admin-worker-pool__pool">
100
+ <strong>{row.name}</strong>
101
+ <span>{row.state.label}</span>
102
+ </div>
103
+ )
104
+ },
105
+ {
106
+ key: "active",
107
+ header: "Capacity",
108
+ width: "34%",
109
+ render: (row) => (
110
+ <div className="eth-admin-worker-pool__capacity">
111
+ <div className="eth-admin-worker-pool__capacity-copy">
112
+ <span>{row.capacityLabel}</span>
113
+ <strong>{row.percentLabel}</strong>
114
+ </div>
115
+ <span
116
+ className="eth-admin-worker-pool__meter"
117
+ role="progressbar"
118
+ aria-label={`${row.name} capacity`}
119
+ aria-valuemin={0}
120
+ aria-valuemax={100}
121
+ aria-valuenow={Math.round(row.cappedPercent)}
122
+ aria-valuetext={`${formatNumber(row.active)} of ${formatNumber(
123
+ row.total
124
+ )} workers active`}
125
+ style={
126
+ {
127
+ "--eth-admin-worker-pool-capacity": `${row.cappedPercent}%`
128
+ } as WorkerPoolMeterStyle
129
+ }
130
+ >
131
+ <span
132
+ className={[
133
+ "eth-admin-worker-pool__meter-fill",
134
+ `eth-admin-worker-pool__meter-fill--${row.state.tone}`
135
+ ].join(" ")}
136
+ />
137
+ </span>
138
+ </div>
139
+ )
140
+ },
141
+ {
142
+ key: "idle",
143
+ header: "Idle",
144
+ align: "end",
145
+ width: "12%",
146
+ render: (row) => (
147
+ <span className="eth-admin-worker-pool__number">{formatNumber(row.idle)}</span>
148
+ )
149
+ },
150
+ {
151
+ key: "failed",
152
+ header: "Failed",
153
+ align: "end",
154
+ width: "12%",
155
+ render: (row) => (
156
+ <Badge severity={row.failed > 0 ? "danger" : "success"}>{formatNumber(row.failed)}</Badge>
157
+ )
158
+ },
159
+ {
160
+ key: "scale",
161
+ header: "Scale",
162
+ align: "end",
163
+ width: "10rem",
164
+ render: (row) => (
165
+ <div
166
+ className="eth-admin-worker-pool__scale"
167
+ role="group"
168
+ aria-label={`${row.name} scale controls`}
169
+ >
170
+ <Button
171
+ type="button"
172
+ intent="ghost"
173
+ density="compact"
174
+ className="eth-admin-worker-pool__scale-button"
175
+ disabled={!onScale}
176
+ aria-label={`Scale ${row.name} down by 1`}
177
+ onClick={() => onScale?.(row.id, -1)}
178
+ >
179
+ -1
180
+ </Button>
181
+ <Button
182
+ type="button"
183
+ intent="ghost"
184
+ density="compact"
185
+ className="eth-admin-worker-pool__scale-button"
186
+ disabled={!onScale}
187
+ aria-label={`Scale ${row.name} up by 1`}
188
+ onClick={() => onScale?.(row.id, 1)}
189
+ >
190
+ +1
191
+ </Button>
192
+ </div>
193
+ )
194
+ }
195
+ ];
196
+
197
+ return (
198
+ <section
199
+ {...sectionProps}
200
+ aria-label={ariaLabel}
201
+ aria-labelledby={ariaLabelledBy ?? (ariaLabel ? undefined : headingId)}
202
+ className={["eth-admin-worker-pool", className].filter(Boolean).join(" ")}
203
+ data-eth-component="WorkerPoolPanel"
204
+ >
205
+ <header>
206
+ <div className="eth-admin-worker-pool__heading">
207
+ <h3 id={headingId}>{title}</h3>
208
+ {supportingText ? <p>{supportingText}</p> : null}
209
+ </div>
210
+ <div className="eth-admin-worker-pool__header-actions">
211
+ <Badge severity={statusSummary.severity}>{statusSummary.label}</Badge>
212
+ <ActionGroup actions={actions} />
213
+ </div>
214
+ </header>
215
+ <dl className="eth-admin-worker-pool__summary" aria-label="Worker pool totals">
216
+ <div>
217
+ <dt>Tracked pools</dt>
218
+ <dd>{formatPoolCount(pools.length)}</dd>
219
+ </div>
220
+ <div>
221
+ <dt>Active capacity</dt>
222
+ <dd>
223
+ {formatNumber(totals.active)} / {formatNumber(totals.total)}
224
+ </dd>
225
+ </div>
226
+ <div>
227
+ <dt>Failures</dt>
228
+ <dd>{formatFailureCount(totals.failed)}</dd>
229
+ </div>
230
+ </dl>
231
+ <DataTable
232
+ rows={rows}
233
+ columns={columns}
234
+ rowKey="id"
235
+ density={density}
236
+ className="eth-admin-worker-pool__table"
237
+ emptyState={
238
+ <p className="eth-admin-worker-pool__empty">No worker pools are configured.</p>
239
+ }
240
+ />
241
+ </section>
242
+ );
243
+ }
244
+
245
+ function workerPoolState(pool: WorkerPool, percent: number): WorkerPoolState {
246
+ if (pool.failed > 0) return { label: "Failure requires review", tone: "danger" };
247
+ if (pool.total <= 0) return { label: "No capacity", tone: "neutral" };
248
+ if (percent >= 100) return { label: "At capacity", tone: "warning" };
249
+ if (pool.idle > 0) return { label: "Capacity available", tone: "normal" };
250
+ return { label: "Active", tone: "normal" };
251
+ }
252
+
253
+ function workerPoolStatusSummary(
254
+ poolCount: number,
255
+ failedCount: number,
256
+ utilization: number
257
+ ): WorkerPoolStatusSummary {
258
+ if (poolCount === 0) return { label: "No pools", severity: "neutral" };
259
+ if (failedCount === 1) return { label: "Failure in pool", severity: "danger" };
260
+ if (failedCount > 1) {
261
+ return { label: `${formatNumber(failedCount)} failures`, severity: "danger" };
262
+ }
263
+ if (utilization >= 90) return { label: "Capacity tight", severity: "warning" };
264
+ return { label: "Operational", severity: "success" };
265
+ }
266
+
267
+ function formatNumber(value: number) {
268
+ return new Intl.NumberFormat("en-US").format(value);
269
+ }
270
+
271
+ function formatPercent(value: number) {
272
+ return `${value.toLocaleString("en-US", { maximumFractionDigits: 0 })}%`;
273
+ }
274
+
275
+ function formatPoolCount(value: number) {
276
+ return `${formatNumber(value)} ${value === 1 ? "pool" : "pools"}`;
277
+ }
278
+
279
+ function formatFailureCount(value: number) {
280
+ return `${formatNumber(value)} failed`;
281
+ }
@@ -0,0 +1,131 @@
1
+ import type * as React from "react";
2
+ import type { Condition, FieldSchema, FormValues, JsonSchema, RuleDefinition } from "@echothink-ui/forms";
3
+
4
+ export type ServiceHealthStatus = "healthy" | "degraded" | "down";
5
+
6
+ export interface HealthService {
7
+ id: string;
8
+ name: string;
9
+ status: ServiceHealthStatus;
10
+ latencyMs?: number;
11
+ incidentsCount?: number;
12
+ }
13
+
14
+ export interface ServiceStatusSummary {
15
+ name: string;
16
+ status: ServiceHealthStatus;
17
+ latencyMs?: number;
18
+ incidentsCount?: number;
19
+ lastIncidentAt?: string;
20
+ uptimePercent?: number;
21
+ }
22
+
23
+ export interface AdminIncident {
24
+ id: string;
25
+ title: string;
26
+ severity: "info" | "warning" | "error" | "critical";
27
+ startedAt: string;
28
+ }
29
+
30
+ export interface AdminMetric {
31
+ label: string;
32
+ value: React.ReactNode;
33
+ trend?: React.ReactNode;
34
+ }
35
+
36
+ export interface WorkerPool {
37
+ id: string;
38
+ name: string;
39
+ active: number;
40
+ total: number;
41
+ idle: number;
42
+ failed: number;
43
+ }
44
+
45
+ export interface JobQueue {
46
+ id: string;
47
+ name: string;
48
+ depth: number;
49
+ processing: number;
50
+ failed: number;
51
+ retryRate?: number;
52
+ }
53
+
54
+ export interface QueueDepthPoint {
55
+ id?: string;
56
+ label?: string;
57
+ timestamp?: string;
58
+ depth: number;
59
+ }
60
+
61
+ export interface LogEntry {
62
+ id: string;
63
+ timestamp: string;
64
+ level?: "debug" | "info" | "warning" | "error";
65
+ source?: string;
66
+ message: string;
67
+ meta?: Record<string, unknown>;
68
+ }
69
+
70
+ export interface AuditLogEntry {
71
+ id: string;
72
+ timestamp: string;
73
+ actor: string;
74
+ action: string;
75
+ resource: string;
76
+ outcome: "success" | "failure" | string;
77
+ ip?: string;
78
+ userAgent?: string;
79
+ }
80
+
81
+ export interface AdminPolicyConfig {
82
+ id?: string;
83
+ name?: string;
84
+ description?: string;
85
+ rules?: RuleDefinition[];
86
+ variables?: string[];
87
+ condition?: Condition;
88
+ [key: string]: unknown;
89
+ }
90
+
91
+ export interface TenantSettingsSection {
92
+ id: string;
93
+ title: string;
94
+ description?: React.ReactNode;
95
+ fields: FieldSchema[];
96
+ }
97
+
98
+ export interface BillingUsageMetric {
99
+ label: string;
100
+ current: number;
101
+ limit: number;
102
+ unit: string;
103
+ trend?: number;
104
+ }
105
+
106
+ export interface FeatureFlag {
107
+ id: string;
108
+ name: string;
109
+ enabled: boolean;
110
+ rolloutPercent?: number;
111
+ environments?: string[];
112
+ }
113
+
114
+ export interface RateLimit {
115
+ id: string;
116
+ name: string;
117
+ current: number;
118
+ limit: number;
119
+ windowSeconds: number;
120
+ }
121
+
122
+ export interface IntegrationHealth {
123
+ id: string;
124
+ name: string;
125
+ status: ServiceHealthStatus;
126
+ incidentsCount?: number;
127
+ latencyMs?: number;
128
+ lastCheckedAt?: string;
129
+ }
130
+
131
+ export type { FormValues, JsonSchema };
package/src/index.tsx ADDED
@@ -0,0 +1,37 @@
1
+ import "./styles.css";
2
+
3
+ export * from "./components/types";
4
+ export * from "./components/AdminShell";
5
+ export * from "./components/SystemHealthDashboard";
6
+ export * from "./components/ServiceStatusCard";
7
+ export * from "./components/WorkerPoolPanel";
8
+ export * from "./components/JobQueuePanel";
9
+ export * from "./components/QueueDepthChart";
10
+ export * from "./components/RuntimeLogViewer";
11
+ export * from "./components/AuditLogViewer";
12
+ export * from "./components/PolicyConfigPanel";
13
+ export * from "./components/BillingUsagePanel";
14
+ export * from "./components/TenantSettingsPanel";
15
+ export * from "./components/FeatureFlagPanel";
16
+ export * from "./components/RateLimitPanel";
17
+ export * from "./components/IntegrationHealthTable";
18
+ export * from "./components/AdminHealthTemplate";
19
+
20
+ export const AdminComponentNames = [
21
+ "AdminShell",
22
+ "SystemHealthDashboard",
23
+ "ServiceStatusCard",
24
+ "WorkerPoolPanel",
25
+ "JobQueuePanel",
26
+ "QueueDepthChart",
27
+ "RuntimeLogViewer",
28
+ "AuditLogViewer",
29
+ "PolicyConfigPanel",
30
+ "BillingUsagePanel",
31
+ "TenantSettingsPanel",
32
+ "FeatureFlagPanel",
33
+ "RateLimitPanel",
34
+ "IntegrationHealthTable",
35
+ "AdminHealthTemplate"
36
+ ] as const;
37
+ export type AdminComponentName = (typeof AdminComponentNames)[number];