@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.
- package/README.md +5 -0
- package/dist/components/AdminHealthTemplate.d.ts +5 -0
- package/dist/components/AdminShell.d.ts +8 -0
- package/dist/components/AuditLogViewer.d.ts +9 -0
- package/dist/components/BillingUsagePanel.d.ts +7 -0
- package/dist/components/FeatureFlagPanel.d.ts +8 -0
- package/dist/components/IntegrationHealthTable.d.ts +6 -0
- package/dist/components/JobQueuePanel.d.ts +7 -0
- package/dist/components/PolicyConfigPanel.d.ts +9 -0
- package/dist/components/QueueDepthChart.d.ts +6 -0
- package/dist/components/RateLimitPanel.d.ts +7 -0
- package/dist/components/RuntimeLogViewer.d.ts +10 -0
- package/dist/components/ServiceStatusCard.d.ts +7 -0
- package/dist/components/SystemHealthDashboard.d.ts +8 -0
- package/dist/components/TenantSettingsPanel.d.ts +11 -0
- package/dist/components/WorkerPoolPanel.d.ts +7 -0
- package/dist/components/types.d.ts +114 -0
- package/dist/index.cjs +1835 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1711 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1812 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/components/AdminHealthTemplate.tsx +25 -0
- package/src/components/AdminShell.tsx +48 -0
- package/src/components/AuditLogViewer.tsx +96 -0
- package/src/components/BillingUsagePanel.tsx +171 -0
- package/src/components/FeatureFlagPanel.tsx +178 -0
- package/src/components/IntegrationHealthTable.tsx +196 -0
- package/src/components/JobQueuePanel.tsx +171 -0
- package/src/components/PolicyConfigPanel.test.tsx +45 -0
- package/src/components/PolicyConfigPanel.tsx +131 -0
- package/src/components/QueueDepthChart.tsx +70 -0
- package/src/components/RateLimitPanel.test.tsx +29 -0
- package/src/components/RateLimitPanel.tsx +249 -0
- package/src/components/RuntimeLogViewer.test.tsx +60 -0
- package/src/components/RuntimeLogViewer.tsx +185 -0
- package/src/components/ServiceStatusCard.tsx +91 -0
- package/src/components/SystemHealthDashboard.tsx +214 -0
- package/src/components/TenantSettingsPanel.test.tsx +38 -0
- package/src/components/TenantSettingsPanel.tsx +44 -0
- package/src/components/WorkerPoolPanel.test.tsx +32 -0
- package/src/components/WorkerPoolPanel.tsx +281 -0
- package/src/components/types.ts +131 -0
- package/src/index.tsx +37 -0
- 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];
|