@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 type { CSSProperties } from "react";
2
+ import { Badge, Surface, type EthSeverity, type SurfaceComponentProps } from "@echothink-ui/core";
3
+ import type { BillingUsageMetric } from "./types";
4
+
5
+ export interface BillingUsagePanelProps extends Omit<SurfaceComponentProps, "children"> {
6
+ metrics: BillingUsageMetric[];
7
+ period: string;
8
+ }
9
+
10
+ type UsageTone = "normal" | "warning" | "danger" | "neutral";
11
+
12
+ interface UsageState {
13
+ label: string;
14
+ severity: EthSeverity;
15
+ tone: UsageTone;
16
+ }
17
+
18
+ type UsageMeterStyle = CSSProperties & {
19
+ "--eth-admin-billing-usage-percent": string;
20
+ };
21
+
22
+ export function BillingUsagePanel({ metrics, period, title, className, ...props }: BillingUsagePanelProps) {
23
+ const entries = metrics.map((metric) => {
24
+ const percent = metric.limit > 0 ? (metric.current / metric.limit) * 100 : 0;
25
+ const roundedPercent = Math.round(percent);
26
+ const cappedPercent = Math.min(100, Math.max(0, percent));
27
+ const state = usageState(percent, metric.limit);
28
+
29
+ return {
30
+ metric,
31
+ percent,
32
+ roundedPercent,
33
+ cappedPercent,
34
+ state,
35
+ trendLabel: formatTrend(metric.trend),
36
+ usageLabel:
37
+ metric.limit > 0
38
+ ? `${formatNumber(metric.current)} / ${formatNumber(metric.limit)} ${formatUnit(metric.unit, metric.limit)}`
39
+ : `${formatNumber(metric.current)} ${formatUnit(metric.unit, metric.current)}`
40
+ };
41
+ });
42
+
43
+ const highestUsage = entries.reduce<(typeof entries)[number] | undefined>((highest, entry) => {
44
+ if (!highest || entry.percent > highest.percent) return entry;
45
+ return highest;
46
+ }, undefined);
47
+ const overLimitCount = entries.filter((entry) => entry.percent >= 100).length;
48
+ const watchedCount = entries.filter((entry) => entry.percent >= 80 && entry.percent < 100).length;
49
+ const quotaStatus = getQuotaStatus(overLimitCount, watchedCount);
50
+
51
+ return (
52
+ <Surface
53
+ {...props}
54
+ title={title ?? "Billing usage"}
55
+ subtitle={period}
56
+ className={["eth-admin-billing-usage", className].filter(Boolean).join(" ")}
57
+ data-eth-component="BillingUsagePanel"
58
+ >
59
+ {entries.length ? (
60
+ <>
61
+ <dl className="eth-admin-billing-usage__summary" aria-label="Billing usage summary">
62
+ <div>
63
+ <dt>Tracked quotas</dt>
64
+ <dd>{entries.length}</dd>
65
+ </div>
66
+ <div>
67
+ <dt>Highest usage</dt>
68
+ <dd>
69
+ {highestUsage ? `${highestUsage.metric.label} ${highestUsage.roundedPercent}%` : "n/a"}
70
+ </dd>
71
+ </div>
72
+ <div>
73
+ <dt>Quota status</dt>
74
+ <dd>
75
+ <Badge severity={quotaStatus.severity}>{quotaStatus.label}</Badge>
76
+ </dd>
77
+ </div>
78
+ </dl>
79
+
80
+ <div className="eth-admin-billing-usage__metrics">
81
+ {entries.map(({ metric, roundedPercent, cappedPercent, state, trendLabel, usageLabel }) => (
82
+ <section key={`${metric.label}-${metric.unit}`} className="eth-admin-billing-usage__metric">
83
+ <div className="eth-admin-billing-usage__metric-header">
84
+ <div>
85
+ <h3>{metric.label}</h3>
86
+ <p>{usageLabel}</p>
87
+ </div>
88
+ <Badge severity={state.severity}>{state.label}</Badge>
89
+ </div>
90
+
91
+ <div
92
+ className="eth-admin-billing-usage__meter"
93
+ role="progressbar"
94
+ aria-label={`${metric.label} quota usage`}
95
+ aria-valuemin={0}
96
+ aria-valuemax={100}
97
+ aria-valuenow={Math.round(cappedPercent)}
98
+ aria-valuetext={
99
+ metric.limit > 0 ? `${roundedPercent}% of quota used` : "No quota limit configured"
100
+ }
101
+ >
102
+ <span
103
+ className={`eth-admin-billing-usage__meter-fill eth-admin-billing-usage__meter-fill--${state.tone}`}
104
+ style={
105
+ {
106
+ "--eth-admin-billing-usage-percent": `${cappedPercent}%`
107
+ } as UsageMeterStyle
108
+ }
109
+ />
110
+ </div>
111
+
112
+ <dl className="eth-admin-billing-usage__metric-details">
113
+ <div>
114
+ <dt>Utilization</dt>
115
+ <dd>{metric.limit > 0 ? `${roundedPercent}%` : "No limit"}</dd>
116
+ </div>
117
+ <div>
118
+ <dt>Limit</dt>
119
+ <dd>
120
+ {metric.limit > 0
121
+ ? `${formatNumber(metric.limit)} ${formatUnit(metric.unit, metric.limit)}`
122
+ : "Unlimited"}
123
+ </dd>
124
+ </div>
125
+ {trendLabel ? (
126
+ <div>
127
+ <dt>Change</dt>
128
+ <dd>{trendLabel}</dd>
129
+ </div>
130
+ ) : null}
131
+ </dl>
132
+ </section>
133
+ ))}
134
+ </div>
135
+ </>
136
+ ) : (
137
+ <p className="eth-admin-billing-usage__empty">No usage metrics are available for this period.</p>
138
+ )}
139
+ </Surface>
140
+ );
141
+ }
142
+
143
+ function usageState(percent: number, limit: number): UsageState {
144
+ if (limit <= 0) return { label: "No limit", severity: "neutral", tone: "neutral" };
145
+ if (percent >= 100) return { label: "Over limit", severity: "danger", tone: "danger" };
146
+ if (percent >= 80) return { label: "Watch", severity: "warning", tone: "warning" };
147
+ return { label: "Within limit", severity: "success", tone: "normal" };
148
+ }
149
+
150
+ function getQuotaStatus(overLimitCount: number, watchedCount: number): Pick<UsageState, "label" | "severity"> {
151
+ if (overLimitCount > 0) return { label: `${overLimitCount} over limit`, severity: "danger" };
152
+ if (watchedCount > 0) return { label: `${watchedCount} nearing limit`, severity: "warning" };
153
+ return { label: "Within limits", severity: "success" };
154
+ }
155
+
156
+ function formatNumber(value: number) {
157
+ return new Intl.NumberFormat("en-US", {
158
+ maximumFractionDigits: value % 1 === 0 ? 0 : 2
159
+ }).format(value);
160
+ }
161
+
162
+ function formatUnit(unit: string, value: number) {
163
+ if (unit.toLowerCase() === "user") return value === 1 ? "user" : "users";
164
+ return unit;
165
+ }
166
+
167
+ function formatTrend(trend?: number) {
168
+ if (trend === undefined) return undefined;
169
+ if (trend === 0) return "Unchanged";
170
+ return `${trend > 0 ? "+" : ""}${trend}% vs previous period`;
171
+ }
@@ -0,0 +1,178 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActionGroup,
4
+ Badge,
5
+ NumberInput,
6
+ Toggle,
7
+ type SurfaceComponentProps
8
+ } from "@echothink-ui/core";
9
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
10
+ import type { FeatureFlag } from "./types";
11
+
12
+ export interface FeatureFlagPanelProps extends Omit<
13
+ SurfaceComponentProps,
14
+ "children" | "onToggle"
15
+ > {
16
+ flags: FeatureFlag[];
17
+ onToggle?: (id: string) => void;
18
+ onUpdate?: (id: string, update: Partial<FeatureFlag>) => void;
19
+ }
20
+
21
+ type FeatureFlagRow = Record<string, unknown> & FeatureFlag;
22
+ type RolloutMeterStyle = React.CSSProperties & {
23
+ "--eth-admin-feature-flag-rollout": string;
24
+ };
25
+
26
+ export function FeatureFlagPanel({
27
+ flags,
28
+ onToggle,
29
+ onUpdate,
30
+ className,
31
+ title = "Feature flags",
32
+ subtitle,
33
+ description,
34
+ actions,
35
+ density = "compact",
36
+ "aria-label": ariaLabel,
37
+ "aria-labelledby": ariaLabelledBy,
38
+ eyebrow: _eyebrow,
39
+ status: _status,
40
+ severity: _severity,
41
+ loading: _loading,
42
+ empty: _empty,
43
+ error: _error,
44
+ items: _items,
45
+ metadata: _metadata,
46
+ footer: _footer,
47
+ ...sectionProps
48
+ }: FeatureFlagPanelProps) {
49
+ const headingId = React.useId();
50
+ const enabledCount = flags.filter((flag) => flag.enabled).length;
51
+ const disabledCount = flags.length - enabledCount;
52
+ const rows: FeatureFlagRow[] = flags.map((flag) => ({ ...flag }));
53
+ const columns: DataColumn<FeatureFlagRow>[] = [
54
+ {
55
+ key: "name",
56
+ header: "Flag",
57
+ width: "34%",
58
+ render: (row) => <span className="eth-admin-feature-flag__name">{row.name}</span>
59
+ },
60
+ {
61
+ key: "enabled",
62
+ header: "Status",
63
+ width: "13rem",
64
+ render: (row) => (
65
+ <div className="eth-admin-feature-flag__status">
66
+ <Toggle
67
+ label={`${row.name} is ${row.enabled ? "enabled" : "disabled"}`}
68
+ hideLabel
69
+ onLabel=""
70
+ offLabel=""
71
+ checked={row.enabled}
72
+ readOnly={!onToggle}
73
+ density={density}
74
+ onChange={() => onToggle?.(row.id)}
75
+ />
76
+ <Badge severity={row.enabled ? "success" : "neutral"}>
77
+ {row.enabled ? "Enabled" : "Disabled"}
78
+ </Badge>
79
+ </div>
80
+ )
81
+ },
82
+ {
83
+ key: "rolloutPercent",
84
+ header: "Rollout",
85
+ width: "18rem",
86
+ render: (row) => (
87
+ <div className="eth-admin-feature-flag__rollout">
88
+ <NumberInput
89
+ aria-label={`${row.name} rollout percent`}
90
+ className="eth-admin-feature-flag__rollout-input"
91
+ density={density}
92
+ hideLabel
93
+ min={0}
94
+ max={100}
95
+ step={1}
96
+ readOnly={!onUpdate}
97
+ value={normalizeRolloutPercent(row.rolloutPercent)}
98
+ onChange={(event) =>
99
+ onUpdate?.(row.id, {
100
+ rolloutPercent: normalizeRolloutPercent(Number(event.currentTarget.value))
101
+ })
102
+ }
103
+ />
104
+ <span className="eth-admin-feature-flag__rollout-unit" aria-hidden="true">
105
+ %
106
+ </span>
107
+ <span className="eth-admin-feature-flag__meter" aria-hidden="true">
108
+ <span
109
+ style={
110
+ {
111
+ "--eth-admin-feature-flag-rollout": `${normalizeRolloutPercent(row.rolloutPercent)}%`
112
+ } as RolloutMeterStyle
113
+ }
114
+ />
115
+ </span>
116
+ </div>
117
+ )
118
+ },
119
+ {
120
+ key: "environments",
121
+ header: "Environments",
122
+ render: (row) =>
123
+ row.environments?.length ? (
124
+ <div className="eth-admin-feature-flag__environments">
125
+ {row.environments.map((environment) => (
126
+ <Badge key={environment} severity="neutral">
127
+ {environment}
128
+ </Badge>
129
+ ))}
130
+ </div>
131
+ ) : (
132
+ <Badge severity="neutral">All environments</Badge>
133
+ )
134
+ }
135
+ ];
136
+ const supportingText = subtitle ?? description;
137
+
138
+ return (
139
+ <section
140
+ {...sectionProps}
141
+ aria-label={ariaLabel}
142
+ aria-labelledby={ariaLabelledBy ?? (ariaLabel ? undefined : headingId)}
143
+ className={["eth-admin-feature-flag", className].filter(Boolean).join(" ")}
144
+ data-eth-component="FeatureFlagPanel"
145
+ >
146
+ <header>
147
+ <div className="eth-admin-feature-flag__heading">
148
+ <h3 id={headingId}>{title}</h3>
149
+ {supportingText ? <p>{supportingText}</p> : null}
150
+ </div>
151
+ <div className="eth-admin-feature-flag__header-actions">
152
+ <div
153
+ className="eth-admin-feature-flag__summary"
154
+ aria-label={`${flags.length} feature flags`}
155
+ >
156
+ <Badge severity="success">{enabledCount} enabled</Badge>
157
+ <Badge severity="neutral">{disabledCount} disabled</Badge>
158
+ </div>
159
+ <ActionGroup actions={actions} />
160
+ </div>
161
+ </header>
162
+ <DataTable
163
+ rows={rows}
164
+ columns={columns}
165
+ density={density}
166
+ rowKey="id"
167
+ className="eth-admin-feature-flag__table"
168
+ emptyState={<p className="eth-admin-feature-flag__empty">No feature flags configured.</p>}
169
+ />
170
+ </section>
171
+ );
172
+ }
173
+
174
+ function normalizeRolloutPercent(value: number | undefined) {
175
+ const numericValue = value ?? 0;
176
+ if (!Number.isFinite(numericValue)) return 0;
177
+ return Math.min(100, Math.max(0, Math.round(numericValue)));
178
+ }
@@ -0,0 +1,196 @@
1
+ import * as React from "react";
2
+ import {
3
+ ActionGroup,
4
+ Badge,
5
+ type EthSeverity,
6
+ type SurfaceComponentProps
7
+ } from "@echothink-ui/core";
8
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
9
+ import type { IntegrationHealth } from "./types";
10
+
11
+ export interface IntegrationHealthTableProps extends Omit<SurfaceComponentProps, "children"> {
12
+ integrations: IntegrationHealth[];
13
+ }
14
+
15
+ type IntegrationHealthRow = Record<string, unknown> & IntegrationHealth;
16
+
17
+ export function IntegrationHealthTable({
18
+ integrations,
19
+ className,
20
+ title = "Integration health",
21
+ subtitle,
22
+ description,
23
+ actions,
24
+ density = "compact",
25
+ "aria-label": ariaLabel,
26
+ "aria-labelledby": ariaLabelledBy,
27
+ eyebrow: _eyebrow,
28
+ status: _status,
29
+ severity: _severity,
30
+ loading: _loading,
31
+ empty: _empty,
32
+ error: _error,
33
+ items: _items,
34
+ metadata: _metadata,
35
+ footer: _footer,
36
+ ...sectionProps
37
+ }: IntegrationHealthTableProps) {
38
+ const headingId = React.useId();
39
+ const incidentCount = integrations.reduce(
40
+ (sum, integration) => sum + (integration.incidentsCount ?? 0),
41
+ 0
42
+ );
43
+ const impairedCount = integrations.filter(
44
+ (integration) => integration.status !== "healthy"
45
+ ).length;
46
+ const healthyCount = integrations.length - impairedCount;
47
+ const rows: IntegrationHealthRow[] = integrations.map((integration) => ({ ...integration }));
48
+ const columns: DataColumn<IntegrationHealthRow>[] = [
49
+ {
50
+ key: "name",
51
+ header: "Integration",
52
+ width: "32%",
53
+ render: (row) => (
54
+ <span className="eth-admin-integration-health__name">
55
+ <strong>{row.name}</strong>
56
+ <span>{row.id}</span>
57
+ </span>
58
+ )
59
+ },
60
+ {
61
+ key: "status",
62
+ header: "Status",
63
+ width: "9rem",
64
+ render: (row) => (
65
+ <Badge severity={statusSeverity(row.status)}>{statusLabel(row.status)}</Badge>
66
+ )
67
+ },
68
+ {
69
+ key: "latencyMs",
70
+ header: "Latency",
71
+ width: "8rem",
72
+ align: "end",
73
+ render: (row) => (
74
+ <span className={latencyMetricClassName(row.latencyMs)}>
75
+ {formatLatency(row.latencyMs)}
76
+ </span>
77
+ )
78
+ },
79
+ {
80
+ key: "incidentsCount",
81
+ header: "Incidents",
82
+ width: "8rem",
83
+ align: "end",
84
+ render: (row) => (
85
+ <Badge severity={incidentSeverity(row.incidentsCount)}>
86
+ {formatIncidentCount(row.incidentsCount)}
87
+ </Badge>
88
+ )
89
+ },
90
+ {
91
+ key: "lastCheckedAt",
92
+ header: "Last checked",
93
+ width: "10rem",
94
+ render: (row) => (
95
+ <span className="eth-admin-integration-health__checked">
96
+ {row.lastCheckedAt ?? "Not checked"}
97
+ </span>
98
+ )
99
+ }
100
+ ];
101
+ const supportingText = subtitle ?? description;
102
+
103
+ return (
104
+ <section
105
+ {...sectionProps}
106
+ aria-label={ariaLabel}
107
+ aria-labelledby={ariaLabelledBy ?? (ariaLabel ? undefined : headingId)}
108
+ className={["eth-admin-integration-health", className].filter(Boolean).join(" ")}
109
+ data-eth-component="IntegrationHealthTable"
110
+ >
111
+ <header>
112
+ <div className="eth-admin-integration-health__heading">
113
+ <h3 id={headingId}>{title}</h3>
114
+ {supportingText ? <p>{supportingText}</p> : null}
115
+ </div>
116
+ <div className="eth-admin-integration-health__header-actions">
117
+ <div
118
+ className="eth-admin-integration-health__summary"
119
+ aria-label={summaryLabel(rows.length, incidentCount, healthyCount)}
120
+ >
121
+ <Badge severity={!rows.length ? "neutral" : impairedCount ? "warning" : "success"}>
122
+ {formatHealthySummary(rows.length, healthyCount)}
123
+ </Badge>
124
+ <Badge severity={incidentCount ? "danger" : "neutral"}>
125
+ {incidentCount} {incidentCount === 1 ? "incident" : "incidents"}
126
+ </Badge>
127
+ </div>
128
+ <ActionGroup actions={actions} />
129
+ </div>
130
+ </header>
131
+ <DataTable
132
+ rows={rows}
133
+ columns={columns}
134
+ density={density}
135
+ rowKey="id"
136
+ className="eth-admin-integration-health__table"
137
+ emptyState={
138
+ <p className="eth-admin-integration-health__empty">No integrations are reporting.</p>
139
+ }
140
+ />
141
+ </section>
142
+ );
143
+ }
144
+
145
+ function statusSeverity(status: IntegrationHealth["status"]): EthSeverity {
146
+ if (status === "healthy") return "success";
147
+ if (status === "degraded") return "warning";
148
+ return "danger";
149
+ }
150
+
151
+ function statusLabel(status: IntegrationHealth["status"]) {
152
+ if (status === "down") return "Down";
153
+ if (status === "degraded") return "Degraded";
154
+ return "Healthy";
155
+ }
156
+
157
+ function latencySeverity(latencyMs: IntegrationHealth["latencyMs"]): EthSeverity | null {
158
+ if (latencyMs === undefined) return null;
159
+ if (latencyMs >= 1000) return "danger";
160
+ if (latencyMs >= 500) return "warning";
161
+ return null;
162
+ }
163
+
164
+ function latencyMetricClassName(latencyMs: IntegrationHealth["latencyMs"]) {
165
+ const severity = latencySeverity(latencyMs);
166
+ return [
167
+ "eth-admin-integration-health__metric",
168
+ severity ? `eth-admin-integration-health__metric--${severity}` : undefined
169
+ ]
170
+ .filter(Boolean)
171
+ .join(" ");
172
+ }
173
+
174
+ function incidentSeverity(count: IntegrationHealth["incidentsCount"]): EthSeverity {
175
+ if (!count) return "success";
176
+ return count > 1 ? "danger" : "warning";
177
+ }
178
+
179
+ function formatLatency(latencyMs: IntegrationHealth["latencyMs"]) {
180
+ return latencyMs === undefined ? "n/a" : `${latencyMs} ms`;
181
+ }
182
+
183
+ function formatIncidentCount(count: IntegrationHealth["incidentsCount"]) {
184
+ return String(count ?? 0);
185
+ }
186
+
187
+ function formatHealthySummary(total: number, healthyCount: number) {
188
+ return total ? `${healthyCount}/${total} healthy` : "0 integrations";
189
+ }
190
+
191
+ function summaryLabel(total: number, incidentCount: number, healthyCount: number) {
192
+ if (!total) return `No integrations reporting, ${incidentCount} open incidents`;
193
+ return `${healthyCount} of ${total} integrations healthy, ${incidentCount} open ${
194
+ incidentCount === 1 ? "incident" : "incidents"
195
+ }`;
196
+ }