@nexus-ai-fs/tui 0.9.18
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 +30 -0
- package/package.json +48 -0
- package/src/app.tsx +349 -0
- package/src/index.tsx +137 -0
- package/src/opentui-env.d.ts +61 -0
- package/src/panels/access/access-panel.tsx +597 -0
- package/src/panels/access/alert-list.tsx +77 -0
- package/src/panels/access/constraint-creator.tsx +128 -0
- package/src/panels/access/constraint-list.tsx +72 -0
- package/src/panels/access/credential-list.tsx +68 -0
- package/src/panels/access/delegation-chain-view.tsx +110 -0
- package/src/panels/access/delegation-completer.tsx +120 -0
- package/src/panels/access/delegation-creator.tsx +237 -0
- package/src/panels/access/delegation-list.tsx +74 -0
- package/src/panels/access/fraud-score-view.tsx +94 -0
- package/src/panels/access/manifest-creator.tsx +167 -0
- package/src/panels/access/manifest-list.tsx +105 -0
- package/src/panels/access/namespace-config-view.tsx +525 -0
- package/src/panels/access/permission-checker.tsx +231 -0
- package/src/panels/agents/agent-status-view.tsx +196 -0
- package/src/panels/agents/agents-panel.tsx +493 -0
- package/src/panels/agents/delegation-list.tsx +154 -0
- package/src/panels/agents/inbox-view.tsx +96 -0
- package/src/panels/agents/trajectories-tab.tsx +40 -0
- package/src/panels/api-console/api-console-panel.tsx +189 -0
- package/src/panels/api-console/codegen-viewer.tsx +36 -0
- package/src/panels/api-console/codegen.ts +112 -0
- package/src/panels/api-console/endpoint-list.tsx +57 -0
- package/src/panels/api-console/request-builder.tsx +69 -0
- package/src/panels/api-console/response-viewer.tsx +54 -0
- package/src/panels/connectors/available-tab.tsx +357 -0
- package/src/panels/connectors/connector-row.tsx +121 -0
- package/src/panels/connectors/connectors-panel.tsx +88 -0
- package/src/panels/connectors/error-parser.ts +116 -0
- package/src/panels/connectors/mounted-tab.tsx +179 -0
- package/src/panels/connectors/skills-tab.tsx +235 -0
- package/src/panels/connectors/template-generator.ts +211 -0
- package/src/panels/connectors/write-tab.tsx +514 -0
- package/src/panels/events/audit-tab.tsx +69 -0
- package/src/panels/events/audit-trail.tsx +75 -0
- package/src/panels/events/connector-detail.tsx +49 -0
- package/src/panels/events/connector-list.tsx +73 -0
- package/src/panels/events/connectors-tab.tsx +92 -0
- package/src/panels/events/event-replay.tsx +80 -0
- package/src/panels/events/events-panel.tsx +414 -0
- package/src/panels/events/events-tab.tsx +212 -0
- package/src/panels/events/lock-list.tsx +54 -0
- package/src/panels/events/locks-tab.tsx +103 -0
- package/src/panels/events/mcl-replay.tsx +77 -0
- package/src/panels/events/mcl-tab.tsx +83 -0
- package/src/panels/events/operations-tab-wrapper.tsx +62 -0
- package/src/panels/events/operations-tab.tsx +41 -0
- package/src/panels/events/replay-tab.tsx +76 -0
- package/src/panels/events/secrets-audit.tsx +64 -0
- package/src/panels/events/secrets-tab.tsx +75 -0
- package/src/panels/events/subscription-list.tsx +54 -0
- package/src/panels/events/subscriptions-tab.tsx +82 -0
- package/src/panels/files/file-aspects.tsx +93 -0
- package/src/panels/files/file-editor.tsx +160 -0
- package/src/panels/files/file-explorer-keybindings.ts +468 -0
- package/src/panels/files/file-explorer-panel.tsx +545 -0
- package/src/panels/files/file-lineage.tsx +163 -0
- package/src/panels/files/file-list-item.tsx +28 -0
- package/src/panels/files/file-metadata.tsx +62 -0
- package/src/panels/files/file-preview.tsx +108 -0
- package/src/panels/files/file-schema.tsx +89 -0
- package/src/panels/files/file-tree-node.tsx +44 -0
- package/src/panels/files/file-tree.tsx +169 -0
- package/src/panels/files/share-links-tab.tsx +33 -0
- package/src/panels/files/uploads-tab.tsx +45 -0
- package/src/panels/payments/approval-list.tsx +83 -0
- package/src/panels/payments/balance-card.tsx +43 -0
- package/src/panels/payments/budget-card.tsx +70 -0
- package/src/panels/payments/payments-panel.tsx +451 -0
- package/src/panels/payments/policy-list.tsx +64 -0
- package/src/panels/payments/reservation-list.tsx +78 -0
- package/src/panels/payments/transaction-list.tsx +103 -0
- package/src/panels/payments/transfer-form.tsx +109 -0
- package/src/panels/search/column-search.tsx +79 -0
- package/src/panels/search/knowledge-view.tsx +100 -0
- package/src/panels/search/memory-list.tsx +197 -0
- package/src/panels/search/playbook-list.tsx +77 -0
- package/src/panels/search/rlm-answer-view.tsx +105 -0
- package/src/panels/search/search-panel.tsx +405 -0
- package/src/panels/search/search-results.tsx +116 -0
- package/src/panels/stack/stack-panel.tsx +474 -0
- package/src/panels/versions/conflicts-tab.tsx +59 -0
- package/src/panels/versions/entry-detail.tsx +89 -0
- package/src/panels/versions/transaction-actions.tsx +34 -0
- package/src/panels/versions/transaction-list.tsx +90 -0
- package/src/panels/versions/versions-panel.tsx +276 -0
- package/src/panels/workflows/execution-list.tsx +102 -0
- package/src/panels/workflows/scheduler-view.tsx +135 -0
- package/src/panels/workflows/workflow-list.tsx +88 -0
- package/src/panels/workflows/workflows-panel.tsx +295 -0
- package/src/panels/zones/brick-detail.tsx +136 -0
- package/src/panels/zones/brick-list.tsx +56 -0
- package/src/panels/zones/cache-tab.tsx +118 -0
- package/src/panels/zones/drift-view.tsx +97 -0
- package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
- package/src/panels/zones/memories-tab.tsx +37 -0
- package/src/panels/zones/reindex-status.tsx +84 -0
- package/src/panels/zones/workspaces-tab.tsx +37 -0
- package/src/panels/zones/zone-list.tsx +73 -0
- package/src/panels/zones/zones-panel.tsx +559 -0
- package/src/services/command-runner.ts +303 -0
- package/src/shared/accessibility-announcements.ts +44 -0
- package/src/shared/action-registry.ts +466 -0
- package/src/shared/brick-states.ts +91 -0
- package/src/shared/command-palette.ts +35 -0
- package/src/shared/components/announcement-bar.tsx +30 -0
- package/src/shared/components/app-confirm-dialog.tsx +29 -0
- package/src/shared/components/breadcrumb.tsx +21 -0
- package/src/shared/components/brick-gate.tsx +60 -0
- package/src/shared/components/command-output.tsx +95 -0
- package/src/shared/components/command-palette.tsx +97 -0
- package/src/shared/components/confirm-dialog.tsx +61 -0
- package/src/shared/components/diff-viewer.tsx +219 -0
- package/src/shared/components/empty-state.tsx +36 -0
- package/src/shared/components/error-bar.tsx +60 -0
- package/src/shared/components/error-boundary.tsx +53 -0
- package/src/shared/components/help-overlay.tsx +99 -0
- package/src/shared/components/identity-switcher.tsx +168 -0
- package/src/shared/components/loading-indicator.tsx +40 -0
- package/src/shared/components/pagination-bar.tsx +68 -0
- package/src/shared/components/pre-connection-screen.tsx +398 -0
- package/src/shared/components/scroll-indicator.tsx +46 -0
- package/src/shared/components/side-nav-utils.ts +68 -0
- package/src/shared/components/side-nav.tsx +287 -0
- package/src/shared/components/spinner.tsx +26 -0
- package/src/shared/components/status-bar.tsx +117 -0
- package/src/shared/components/styled-text.tsx +72 -0
- package/src/shared/components/sub-tab-bar-utils.ts +100 -0
- package/src/shared/components/sub-tab-bar.tsx +40 -0
- package/src/shared/components/tab-bar-utils.ts +36 -0
- package/src/shared/components/tab-bar.tsx +50 -0
- package/src/shared/components/text-input.tsx +73 -0
- package/src/shared/components/tooltip.tsx +53 -0
- package/src/shared/components/virtual-list.tsx +93 -0
- package/src/shared/components/welcome-screen.tsx +111 -0
- package/src/shared/hooks/use-api.ts +10 -0
- package/src/shared/hooks/use-brick-available.ts +42 -0
- package/src/shared/hooks/use-confirm.ts +66 -0
- package/src/shared/hooks/use-connection-state.ts +67 -0
- package/src/shared/hooks/use-copy.ts +31 -0
- package/src/shared/hooks/use-fresh-server.ts +62 -0
- package/src/shared/hooks/use-keyboard.ts +58 -0
- package/src/shared/hooks/use-list-navigation.ts +106 -0
- package/src/shared/hooks/use-swr.ts +117 -0
- package/src/shared/hooks/use-tab-fallback.ts +32 -0
- package/src/shared/hooks/use-text-input.ts +113 -0
- package/src/shared/hooks/use-visible-tabs.ts +61 -0
- package/src/shared/lib/circular-buffer.ts +82 -0
- package/src/shared/lib/clipboard.ts +14 -0
- package/src/shared/nav-items.ts +73 -0
- package/src/shared/navigation.ts +110 -0
- package/src/shared/status-breadcrumb.ts +74 -0
- package/src/shared/syntax-style.ts +3 -0
- package/src/shared/tab-visibility.ts +15 -0
- package/src/shared/text-style.ts +23 -0
- package/src/shared/theme.ts +179 -0
- package/src/shared/utils/format-size.ts +20 -0
- package/src/shared/utils/format-text.ts +10 -0
- package/src/shared/utils/format-time.ts +72 -0
- package/src/shared/utils/lru-cache.ts +75 -0
- package/src/stores/access-store-types.ts +154 -0
- package/src/stores/access-store.ts +674 -0
- package/src/stores/agents-store.ts +404 -0
- package/src/stores/announcement-store.ts +46 -0
- package/src/stores/api-console-store.ts +476 -0
- package/src/stores/connectors-store.ts +434 -0
- package/src/stores/create-api-action.ts +140 -0
- package/src/stores/delegation-store.ts +300 -0
- package/src/stores/error-store.ts +102 -0
- package/src/stores/events-store.ts +163 -0
- package/src/stores/files-store.ts +630 -0
- package/src/stores/first-run-store.ts +34 -0
- package/src/stores/global-store.ts +255 -0
- package/src/stores/infra-store.ts +461 -0
- package/src/stores/knowledge-store.ts +358 -0
- package/src/stores/lineage-store.ts +126 -0
- package/src/stores/mcp-store.ts +147 -0
- package/src/stores/payments-store.ts +545 -0
- package/src/stores/search-store-types.ts +155 -0
- package/src/stores/search-store.ts +656 -0
- package/src/stores/share-link-store.ts +151 -0
- package/src/stores/stack-store.ts +352 -0
- package/src/stores/ui-store.ts +161 -0
- package/src/stores/upload-store.ts +131 -0
- package/src/stores/versions-store.ts +355 -0
- package/src/stores/workflows-store.ts +402 -0
- package/src/stores/workspace-store.ts +185 -0
- package/src/stores/zones-store.ts +378 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control panel: tabbed layout for manifests, alerts,
|
|
3
|
+
* credentials, fraud scores, and delegations.
|
|
4
|
+
*
|
|
5
|
+
* Key bindings:
|
|
6
|
+
* j/k or up/down : navigate within lists
|
|
7
|
+
* g / Shift+G : jump to start / end of list
|
|
8
|
+
* Tab : cycle tabs
|
|
9
|
+
* Escape : close overlay
|
|
10
|
+
* Enter : manifests -> fetch detail (tuple entries)
|
|
11
|
+
* p : open permission checker (+ governance edge check)
|
|
12
|
+
* Shift+R : (alerts tab) resolve selected
|
|
13
|
+
* c : (manifests tab) create new manifest; (fraud tab) compute fraud scores
|
|
14
|
+
* Shift+X : (manifests tab) revoke selected manifest
|
|
15
|
+
* n : (delegations tab) create new delegation
|
|
16
|
+
* x : (delegations tab) revoke selected delegation
|
|
17
|
+
* o : (delegations tab) complete selected delegation
|
|
18
|
+
* v : (delegations tab) view delegation chain
|
|
19
|
+
* w : (delegations tab) view namespace config
|
|
20
|
+
* r : refresh current tab
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
24
|
+
import { useAccessStore } from "../../stores/access-store.js";
|
|
25
|
+
import type { AccessTab } from "../../stores/access-store.js";
|
|
26
|
+
import { useGlobalStore } from "../../stores/global-store.js";
|
|
27
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
28
|
+
import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
|
|
29
|
+
import { useCopy } from "../../shared/hooks/use-copy.js";
|
|
30
|
+
import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
|
|
31
|
+
import { useApi } from "../../shared/hooks/use-api.js";
|
|
32
|
+
import { useUiStore } from "../../stores/ui-store.js";
|
|
33
|
+
import { useVisibleTabs, type TabDef } from "../../shared/hooks/use-visible-tabs.js";
|
|
34
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
35
|
+
import { statusColor } from "../../shared/theme.js";
|
|
36
|
+
import { textStyle } from "../../shared/text-style.js";
|
|
37
|
+
import { ManifestList } from "./manifest-list.js";
|
|
38
|
+
import { AlertList } from "./alert-list.js";
|
|
39
|
+
import { CredentialList } from "./credential-list.js";
|
|
40
|
+
import { FraudScoreView } from "./fraud-score-view.js";
|
|
41
|
+
import { DelegationList } from "./delegation-list.js";
|
|
42
|
+
import { PermissionChecker } from "./permission-checker.js";
|
|
43
|
+
import { DelegationCreator } from "./delegation-creator.js";
|
|
44
|
+
import { DelegationCompleter } from "./delegation-completer.js";
|
|
45
|
+
import { DelegationChainView } from "./delegation-chain-view.js";
|
|
46
|
+
import { NamespaceConfigView } from "./namespace-config-view.js";
|
|
47
|
+
import { ManifestCreator } from "./manifest-creator.js";
|
|
48
|
+
import { ConstraintList } from "./constraint-list.js";
|
|
49
|
+
import { ConstraintCreator } from "./constraint-creator.js";
|
|
50
|
+
|
|
51
|
+
const ALL_TABS: readonly TabDef<AccessTab>[] = [
|
|
52
|
+
{ id: "manifests", label: "Manifests", brick: "access_manifest" },
|
|
53
|
+
{ id: "alerts", label: "Alerts", brick: "governance" },
|
|
54
|
+
{ id: "credentials", label: "Credentials", brick: "auth" },
|
|
55
|
+
{ id: "fraud", label: "Fraud", brick: "governance" },
|
|
56
|
+
{ id: "delegations", label: "Delegations", brick: "delegation" },
|
|
57
|
+
];
|
|
58
|
+
const TAB_LABELS: Readonly<Record<AccessTab, string>> = {
|
|
59
|
+
manifests: "Manifests",
|
|
60
|
+
alerts: "Alerts",
|
|
61
|
+
credentials: "Credentials",
|
|
62
|
+
fraud: "Fraud",
|
|
63
|
+
delegations: "Delegations",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type OverlayMode =
|
|
67
|
+
| "none"
|
|
68
|
+
| "permissionChecker"
|
|
69
|
+
| "delegationCreator"
|
|
70
|
+
| "delegationCompleter"
|
|
71
|
+
| "delegationChainView"
|
|
72
|
+
| "namespaceConfigView"
|
|
73
|
+
| "manifestCreator"
|
|
74
|
+
| "constraintCreator";
|
|
75
|
+
|
|
76
|
+
export default function AccessPanel(): React.ReactNode {
|
|
77
|
+
const client = useApi();
|
|
78
|
+
const confirm = useConfirmStore((s) => s.confirm);
|
|
79
|
+
const overlayActive = useUiStore((s) => s.overlayActive);
|
|
80
|
+
const visibleTabs = useVisibleTabs(ALL_TABS);
|
|
81
|
+
const { copy, copied } = useCopy();
|
|
82
|
+
const [overlay, setOverlay] = useState<OverlayMode>("none");
|
|
83
|
+
|
|
84
|
+
// Zone for fraud score queries
|
|
85
|
+
const configZoneId = useGlobalStore((s) => s.config.zoneId);
|
|
86
|
+
const serverZoneId = useGlobalStore((s) => s.zoneId);
|
|
87
|
+
const effectiveZoneId = configZoneId ?? serverZoneId ?? undefined;
|
|
88
|
+
|
|
89
|
+
const manifests = useAccessStore((s) => s.manifests);
|
|
90
|
+
const selectedManifestIndex = useAccessStore((s) => s.selectedManifestIndex);
|
|
91
|
+
const manifestsLoading = useAccessStore((s) => s.manifestsLoading);
|
|
92
|
+
const lastPermissionCheck = useAccessStore((s) => s.lastPermissionCheck);
|
|
93
|
+
const permissionCheckLoading = useAccessStore((s) => s.permissionCheckLoading);
|
|
94
|
+
const alerts = useAccessStore((s) => s.alerts);
|
|
95
|
+
const alertsLoading = useAccessStore((s) => s.alertsLoading);
|
|
96
|
+
const selectedAlertIndex = useAccessStore((s) => s.selectedAlertIndex);
|
|
97
|
+
const credentials = useAccessStore((s) => s.credentials);
|
|
98
|
+
const credentialsLoading = useAccessStore((s) => s.credentialsLoading);
|
|
99
|
+
const fraudScores = useAccessStore((s) => s.fraudScores);
|
|
100
|
+
const fraudScoresLoading = useAccessStore((s) => s.fraudScoresLoading);
|
|
101
|
+
const selectedFraudIndex = useAccessStore((s) => s.selectedFraudIndex);
|
|
102
|
+
const delegations = useAccessStore((s) => s.delegations);
|
|
103
|
+
const delegationsLoading = useAccessStore((s) => s.delegationsLoading);
|
|
104
|
+
const selectedDelegationIndex = useAccessStore((s) => s.selectedDelegationIndex);
|
|
105
|
+
const governanceCheck = useAccessStore((s) => s.governanceCheck);
|
|
106
|
+
const governanceCheckLoading = useAccessStore((s) => s.governanceCheckLoading);
|
|
107
|
+
const activeTab = useAccessStore((s) => s.activeTab);
|
|
108
|
+
const error = useAccessStore((s) => s.error);
|
|
109
|
+
|
|
110
|
+
const fetchManifests = useAccessStore((s) => s.fetchManifests);
|
|
111
|
+
const fetchManifestDetail = useAccessStore((s) => s.fetchManifestDetail);
|
|
112
|
+
const revokeManifest = useAccessStore((s) => s.revokeManifest);
|
|
113
|
+
const fetchAlerts = useAccessStore((s) => s.fetchAlerts);
|
|
114
|
+
const resolveAlert = useAccessStore((s) => s.resolveAlert);
|
|
115
|
+
const fetchCredentials = useAccessStore((s) => s.fetchCredentials);
|
|
116
|
+
const issueCredential = useAccessStore((s) => s.issueCredential);
|
|
117
|
+
const collusionRings = useAccessStore((s) => s.collusionRings);
|
|
118
|
+
const collusionLoading = useAccessStore((s) => s.collusionLoading);
|
|
119
|
+
const fetchCollusionRings = useAccessStore((s) => s.fetchCollusionRings);
|
|
120
|
+
const suspendAgent = useAccessStore((s) => s.suspendAgent);
|
|
121
|
+
const constraints = useAccessStore((s) => s.constraints);
|
|
122
|
+
const constraintsLoading = useAccessStore((s) => s.constraintsLoading);
|
|
123
|
+
const selectedConstraintIndex = useAccessStore((s) => s.selectedConstraintIndex);
|
|
124
|
+
const fetchConstraints = useAccessStore((s) => s.fetchConstraints);
|
|
125
|
+
const deleteConstraint = useAccessStore((s) => s.deleteConstraint);
|
|
126
|
+
const setSelectedConstraintIndex = useAccessStore((s) => s.setSelectedConstraintIndex);
|
|
127
|
+
const revokeCredential = useAccessStore((s) => s.revokeCredential);
|
|
128
|
+
const fetchFraudScores = useAccessStore((s) => s.fetchFraudScores);
|
|
129
|
+
const computeFraudScores = useAccessStore((s) => s.computeFraudScores);
|
|
130
|
+
const fetchDelegations = useAccessStore((s) => s.fetchDelegations);
|
|
131
|
+
const revokeDelegation = useAccessStore((s) => s.revokeDelegation);
|
|
132
|
+
const setActiveTab = useAccessStore((s) => s.setActiveTab);
|
|
133
|
+
const setSelectedManifestIndex = useAccessStore((s) => s.setSelectedManifestIndex);
|
|
134
|
+
const setSelectedAlertIndex = useAccessStore((s) => s.setSelectedAlertIndex);
|
|
135
|
+
const setSelectedFraudIndex = useAccessStore((s) => s.setSelectedFraudIndex);
|
|
136
|
+
const setSelectedDelegationIndex = useAccessStore((s) => s.setSelectedDelegationIndex);
|
|
137
|
+
|
|
138
|
+
// Credential selection index
|
|
139
|
+
const [selectedCredentialIndex, setSelectedCredentialIndex] = useState(0);
|
|
140
|
+
|
|
141
|
+
// Clamp selectedCredentialIndex when credentials list shrinks (e.g. after revoke)
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (credentials.length > 0 && selectedCredentialIndex >= credentials.length) {
|
|
144
|
+
setSelectedCredentialIndex(Math.max(0, credentials.length - 1));
|
|
145
|
+
}
|
|
146
|
+
}, [credentials.length, selectedCredentialIndex]);
|
|
147
|
+
|
|
148
|
+
// Fraud tab: which list is focused (scores vs constraints)
|
|
149
|
+
const [fraudFocus, setFraudFocus] = useState<"scores" | "constraints">("scores");
|
|
150
|
+
|
|
151
|
+
// Delegation status filter
|
|
152
|
+
const [delegationFilter, setDelegationFilter] = useState<string | null>(null);
|
|
153
|
+
|
|
154
|
+
// Fall back to first visible tab if the active tab becomes hidden
|
|
155
|
+
const visibleIds = visibleTabs.map((t) => t.id);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (visibleIds.length > 0 && !visibleIds.includes(activeTab)) {
|
|
158
|
+
setActiveTab(visibleIds[0]!);
|
|
159
|
+
}
|
|
160
|
+
}, [visibleIds.join(","), activeTab, setActiveTab]);
|
|
161
|
+
|
|
162
|
+
// Refresh current view based on active tab
|
|
163
|
+
const refreshCurrentView = useCallback((): void => {
|
|
164
|
+
if (!client) return;
|
|
165
|
+
|
|
166
|
+
if (activeTab === "manifests") {
|
|
167
|
+
fetchManifests(client);
|
|
168
|
+
} else if (activeTab === "alerts") {
|
|
169
|
+
fetchAlerts(effectiveZoneId, client);
|
|
170
|
+
} else if (activeTab === "credentials") {
|
|
171
|
+
const selected = manifests[selectedManifestIndex];
|
|
172
|
+
if (selected) {
|
|
173
|
+
fetchCredentials(selected.agent_id, client);
|
|
174
|
+
}
|
|
175
|
+
} else if (activeTab === "fraud") {
|
|
176
|
+
fetchFraudScores(effectiveZoneId, client);
|
|
177
|
+
if (effectiveZoneId) fetchConstraints(effectiveZoneId, client);
|
|
178
|
+
} else if (activeTab === "delegations") {
|
|
179
|
+
fetchDelegations(client, delegationFilter);
|
|
180
|
+
}
|
|
181
|
+
}, [client, activeTab, manifests, selectedManifestIndex, effectiveZoneId, delegationFilter, fetchManifests, fetchAlerts, fetchCredentials, fetchFraudScores, fetchDelegations]);
|
|
182
|
+
|
|
183
|
+
// Auto-fetch when tab changes
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
refreshCurrentView();
|
|
186
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
187
|
+
}, [activeTab, client]);
|
|
188
|
+
|
|
189
|
+
// Shared list navigation (j/k/up/down/g/G) — switches per active tab
|
|
190
|
+
const listNav = listNavigationBindings({
|
|
191
|
+
getIndex: () => {
|
|
192
|
+
if (activeTab === "manifests") return selectedManifestIndex;
|
|
193
|
+
if (activeTab === "alerts") return selectedAlertIndex;
|
|
194
|
+
if (activeTab === "credentials") return selectedCredentialIndex;
|
|
195
|
+
if (activeTab === "fraud") return fraudFocus === "scores" ? selectedFraudIndex : selectedConstraintIndex;
|
|
196
|
+
if (activeTab === "delegations") return selectedDelegationIndex;
|
|
197
|
+
return 0;
|
|
198
|
+
},
|
|
199
|
+
setIndex: (i) => {
|
|
200
|
+
if (activeTab === "manifests") setSelectedManifestIndex(i);
|
|
201
|
+
else if (activeTab === "alerts") setSelectedAlertIndex(i);
|
|
202
|
+
else if (activeTab === "credentials") setSelectedCredentialIndex(i);
|
|
203
|
+
else if (activeTab === "fraud") {
|
|
204
|
+
if (fraudFocus === "scores") setSelectedFraudIndex(i);
|
|
205
|
+
else setSelectedConstraintIndex(i);
|
|
206
|
+
} else if (activeTab === "delegations") setSelectedDelegationIndex(i);
|
|
207
|
+
},
|
|
208
|
+
getLength: () => {
|
|
209
|
+
if (activeTab === "manifests") return manifests.length;
|
|
210
|
+
if (activeTab === "alerts") return alerts.length;
|
|
211
|
+
if (activeTab === "credentials") return credentials.length;
|
|
212
|
+
if (activeTab === "fraud") return fraudFocus === "scores" ? fraudScores.length : constraints.length;
|
|
213
|
+
if (activeTab === "delegations") return delegations.length;
|
|
214
|
+
return 0;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
useKeyboard(overlayActive ? {} : {
|
|
219
|
+
...listNav,
|
|
220
|
+
...subTabCycleBindings(visibleTabs, activeTab, setActiveTab),
|
|
221
|
+
escape: () => {
|
|
222
|
+
if (overlay !== "none") {
|
|
223
|
+
setOverlay("none");
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
tab: () => {
|
|
227
|
+
if (activeTab === "fraud") {
|
|
228
|
+
setFraudFocus((f) => f === "scores" ? "constraints" : "scores");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const ids = visibleTabs.map((t) => t.id);
|
|
232
|
+
const currentIdx = ids.indexOf(activeTab);
|
|
233
|
+
const nextIdx = (currentIdx + 1) % ids.length;
|
|
234
|
+
const nextTab = ids[nextIdx];
|
|
235
|
+
if (nextTab) {
|
|
236
|
+
setActiveTab(nextTab);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"shift+tab": () => {
|
|
240
|
+
const ids = visibleTabs.map((t) => t.id);
|
|
241
|
+
const currentIdx = ids.indexOf(activeTab);
|
|
242
|
+
const nextIdx = (currentIdx + 1) % ids.length;
|
|
243
|
+
const nextTab = ids[nextIdx];
|
|
244
|
+
if (nextTab) {
|
|
245
|
+
setActiveTab(nextTab);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
return: () => {
|
|
249
|
+
// Manifests: fetch detail to load tuple entries
|
|
250
|
+
if (activeTab === "manifests" && client) {
|
|
251
|
+
const selected = manifests[selectedManifestIndex];
|
|
252
|
+
if (selected) {
|
|
253
|
+
fetchManifestDetail(selected.manifest_id, client);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
r: () => refreshCurrentView(),
|
|
258
|
+
p: () => {
|
|
259
|
+
if (overlay === "none") {
|
|
260
|
+
setOverlay("permissionChecker");
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
n: () => {
|
|
264
|
+
if (activeTab === "delegations" && overlay === "none") {
|
|
265
|
+
setOverlay("delegationCreator");
|
|
266
|
+
} else if (activeTab === "fraud" && overlay === "none") {
|
|
267
|
+
setOverlay("constraintCreator");
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
d: async () => {
|
|
271
|
+
if (activeTab === "fraud" && overlay === "none" && client) {
|
|
272
|
+
const selected = constraints[selectedConstraintIndex];
|
|
273
|
+
if (selected) {
|
|
274
|
+
const ok = await confirm("Delete constraint?", `Delete governance constraint from ${selected.from_agent_id} to ${selected.to_agent_id} [${selected.constraint_type}].`);
|
|
275
|
+
if (!ok) return;
|
|
276
|
+
deleteConstraint(selected.id, client);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
x: async () => {
|
|
281
|
+
if (activeTab === "delegations" && overlay === "none" && client) {
|
|
282
|
+
const selected = delegations[selectedDelegationIndex];
|
|
283
|
+
if (selected && selected.status === "active") {
|
|
284
|
+
revokeDelegation(selected.delegation_id, client);
|
|
285
|
+
}
|
|
286
|
+
} else if (activeTab === "credentials" && overlay === "none" && client) {
|
|
287
|
+
const selected = credentials[selectedCredentialIndex];
|
|
288
|
+
if (selected && selected.is_active) {
|
|
289
|
+
const ok = await confirm("Revoke credential?", "Revoke this credential. The holder will lose access.");
|
|
290
|
+
if (!ok) return;
|
|
291
|
+
revokeCredential(selected.credential_id, selected.subject_agent_id, client);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
o: () => {
|
|
296
|
+
if (activeTab === "delegations" && overlay === "none") {
|
|
297
|
+
const selected = delegations[selectedDelegationIndex];
|
|
298
|
+
if (selected && selected.status === "active") {
|
|
299
|
+
setOverlay("delegationCompleter");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
v: () => {
|
|
304
|
+
if (activeTab === "delegations" && overlay === "none") {
|
|
305
|
+
const selected = delegations[selectedDelegationIndex];
|
|
306
|
+
if (selected) {
|
|
307
|
+
setOverlay("delegationChainView");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
w: () => {
|
|
312
|
+
if (activeTab === "delegations" && overlay === "none") {
|
|
313
|
+
const selected = delegations[selectedDelegationIndex];
|
|
314
|
+
if (selected) {
|
|
315
|
+
setOverlay("namespaceConfigView");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
c: () => {
|
|
320
|
+
if (activeTab === "manifests" && overlay === "none") {
|
|
321
|
+
setOverlay("manifestCreator");
|
|
322
|
+
} else if (activeTab === "fraud" && client) {
|
|
323
|
+
// Compute fraud scores
|
|
324
|
+
computeFraudScores(effectiveZoneId, client);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"shift+x": async () => {
|
|
328
|
+
if (activeTab === "manifests" && overlay === "none" && client) {
|
|
329
|
+
const selected = manifests[selectedManifestIndex];
|
|
330
|
+
if (selected && selected.status === "active") {
|
|
331
|
+
const ok = await confirm("Revoke manifest?", "Revoke this access manifest. Active sessions may be terminated.");
|
|
332
|
+
if (!ok) return;
|
|
333
|
+
revokeManifest(selected.manifest_id, client);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
"shift+r": () => {
|
|
338
|
+
if (!client || overlay !== "none") return;
|
|
339
|
+
if (activeTab === "alerts") {
|
|
340
|
+
const selected = alerts[selectedAlertIndex];
|
|
341
|
+
if (selected && !selected.resolved) {
|
|
342
|
+
resolveAlert(selected.alert_id, "tui-operator", effectiveZoneId, client);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
f: () => {
|
|
347
|
+
if (activeTab === "delegations") {
|
|
348
|
+
const cycle: (string | null)[] = [null, "active", "revoked", "expired", "completed"];
|
|
349
|
+
const idx = cycle.indexOf(delegationFilter);
|
|
350
|
+
const next = cycle[(idx + 1) % cycle.length] ?? null;
|
|
351
|
+
setDelegationFilter(next);
|
|
352
|
+
if (client) fetchDelegations(client, next);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
i: () => {
|
|
356
|
+
// Issue credential for the selected agent (from manifests tab's agent_id)
|
|
357
|
+
if (activeTab === "credentials" && client) {
|
|
358
|
+
const manifest = manifests[selectedManifestIndex];
|
|
359
|
+
if (manifest) {
|
|
360
|
+
issueCredential(manifest.agent_id, {}, client);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
y: () => {
|
|
365
|
+
if (activeTab === "manifests") {
|
|
366
|
+
const selected = manifests[selectedManifestIndex];
|
|
367
|
+
if (selected) copy(selected.manifest_id);
|
|
368
|
+
} else if (activeTab === "delegations") {
|
|
369
|
+
const selected = delegations[selectedDelegationIndex];
|
|
370
|
+
if (selected) copy(selected.delegation_id);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
"shift+c": () => {
|
|
374
|
+
// Fetch collusion rings (fraud tab)
|
|
375
|
+
if (activeTab === "fraud" && client) {
|
|
376
|
+
fetchCollusionRings(effectiveZoneId, client);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
s: async () => {
|
|
380
|
+
// Suspend selected agent (fraud tab — selected by fraud score index)
|
|
381
|
+
if (activeTab === "fraud" && client) {
|
|
382
|
+
const selected = fraudScores[selectedFraudIndex];
|
|
383
|
+
if (selected) {
|
|
384
|
+
const ok = await confirm("Suspend agent?", "Suspend this agent. It will be unable to act until unsuspended.");
|
|
385
|
+
if (!ok) return;
|
|
386
|
+
suspendAgent(selected.agent_id, "Suspended via TUI", effectiveZoneId, client);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Derive selected items for overlays
|
|
393
|
+
const selectedManifest = manifests[selectedManifestIndex];
|
|
394
|
+
const initialManifestId = selectedManifest?.manifest_id ?? "";
|
|
395
|
+
const selectedDelegation = delegations[selectedDelegationIndex];
|
|
396
|
+
|
|
397
|
+
const closeOverlay = (): void => setOverlay("none");
|
|
398
|
+
|
|
399
|
+
const OVERLAY_LABELS: Readonly<Record<OverlayMode, string>> = {
|
|
400
|
+
none: "",
|
|
401
|
+
permissionChecker: " | Permission Checker",
|
|
402
|
+
delegationCreator: " | New Delegation",
|
|
403
|
+
delegationCompleter: " | Complete Delegation",
|
|
404
|
+
delegationChainView: " | Delegation Chain",
|
|
405
|
+
namespaceConfigView: " | Namespace Editor",
|
|
406
|
+
manifestCreator: " | New Manifest",
|
|
407
|
+
constraintCreator: " | New Constraint",
|
|
408
|
+
};
|
|
409
|
+
const overlayLabel = OVERLAY_LABELS[overlay];
|
|
410
|
+
|
|
411
|
+
if (overlay !== "none") {
|
|
412
|
+
return (
|
|
413
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
414
|
+
<box height={1} width="100%">
|
|
415
|
+
<text>
|
|
416
|
+
{visibleTabs.map((tab) => {
|
|
417
|
+
return tab.id === activeTab ? `[${tab.label}]` : ` ${tab.label} `;
|
|
418
|
+
}).join(" ")}
|
|
419
|
+
{overlayLabel}
|
|
420
|
+
</text>
|
|
421
|
+
</box>
|
|
422
|
+
<box flexGrow={1} borderStyle="single">
|
|
423
|
+
{overlay === "permissionChecker" && (
|
|
424
|
+
<PermissionChecker
|
|
425
|
+
initialManifestId={initialManifestId}
|
|
426
|
+
lastResult={lastPermissionCheck}
|
|
427
|
+
loading={permissionCheckLoading}
|
|
428
|
+
governanceCheck={governanceCheck}
|
|
429
|
+
governanceCheckLoading={governanceCheckLoading}
|
|
430
|
+
zoneId={effectiveZoneId}
|
|
431
|
+
onClose={closeOverlay}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
{overlay === "delegationCreator" && (
|
|
435
|
+
<DelegationCreator onClose={closeOverlay} />
|
|
436
|
+
)}
|
|
437
|
+
{overlay === "delegationCompleter" && (
|
|
438
|
+
<DelegationCompleter
|
|
439
|
+
delegationId={selectedDelegation?.delegation_id ?? ""}
|
|
440
|
+
onClose={closeOverlay}
|
|
441
|
+
/>
|
|
442
|
+
)}
|
|
443
|
+
{overlay === "delegationChainView" && (
|
|
444
|
+
<DelegationChainView
|
|
445
|
+
delegationId={selectedDelegation?.delegation_id ?? ""}
|
|
446
|
+
onClose={closeOverlay}
|
|
447
|
+
/>
|
|
448
|
+
)}
|
|
449
|
+
{overlay === "namespaceConfigView" && (
|
|
450
|
+
<NamespaceConfigView
|
|
451
|
+
delegationId={selectedDelegation?.delegation_id ?? ""}
|
|
452
|
+
onClose={closeOverlay}
|
|
453
|
+
/>
|
|
454
|
+
)}
|
|
455
|
+
{overlay === "manifestCreator" && (
|
|
456
|
+
<ManifestCreator onClose={closeOverlay} />
|
|
457
|
+
)}
|
|
458
|
+
{overlay === "constraintCreator" && (
|
|
459
|
+
<ConstraintCreator
|
|
460
|
+
zoneId={effectiveZoneId ?? ""}
|
|
461
|
+
onClose={closeOverlay}
|
|
462
|
+
/>
|
|
463
|
+
)}
|
|
464
|
+
</box>
|
|
465
|
+
</box>
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Tab-specific help text
|
|
470
|
+
const delegationFilterLabel = delegationFilter ? ` [${delegationFilter}]` : "";
|
|
471
|
+
const HELP: Readonly<Record<AccessTab, string>> = {
|
|
472
|
+
manifests: "j/k:navigate g/G:jump Enter:show entries c:new manifest Shift+X:revoke p:perm check y:copy Esc:close Tab:tab r:refresh q:quit",
|
|
473
|
+
alerts: "j/k:navigate g/G:jump Shift+R:resolve Esc:close Tab:tab r:refresh q:quit",
|
|
474
|
+
credentials: "j/k:navigate g/G:jump i:issue x:revoke Esc:close Tab:tab r:refresh q:quit",
|
|
475
|
+
fraud: "j/k:navigate g/G:jump c:compute Shift+C:collusion s:suspend n:new constraint d:delete Tab:focus Shift+Tab:tab Esc:close r:refresh q:quit",
|
|
476
|
+
delegations: `j/k:navigate g/G:jump n:new x:revoke o:complete v:chain w:namespace y:copy f:filter${delegationFilterLabel} Esc:close Tab:tab r:refresh q:quit`,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
481
|
+
{/* Tab bar */}
|
|
482
|
+
<box height={1} width="100%">
|
|
483
|
+
<text>
|
|
484
|
+
{visibleTabs.map((tab) => {
|
|
485
|
+
return tab.id === activeTab ? `[${tab.label}]` : ` ${tab.label} `;
|
|
486
|
+
}).join(" ")}
|
|
487
|
+
</text>
|
|
488
|
+
</box>
|
|
489
|
+
|
|
490
|
+
{/* Permission evaluation result */}
|
|
491
|
+
{lastPermissionCheck && (
|
|
492
|
+
<box height={3} width="100%" borderStyle="single" borderColor={lastPermissionCheck.permission === "allow" ? statusColor.healthy : statusColor.error}>
|
|
493
|
+
<text style={textStyle({ fg: lastPermissionCheck.permission === "allow" ? statusColor.healthy : statusColor.error })}>
|
|
494
|
+
{` ${lastPermissionCheck.permission === "allow" ? "[ALLOW]" : "[DENY] "} tool=${lastPermissionCheck.tool_name} agent=${lastPermissionCheck.agent_id} manifest=${lastPermissionCheck.manifest_id}`}
|
|
495
|
+
</text>
|
|
496
|
+
</box>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{/* Error display */}
|
|
500
|
+
{error && (
|
|
501
|
+
<box height={1} width="100%">
|
|
502
|
+
<text>{`Error: ${error}`}</text>
|
|
503
|
+
</box>
|
|
504
|
+
)}
|
|
505
|
+
|
|
506
|
+
{/* Detail content */}
|
|
507
|
+
<box flexGrow={1} borderStyle="single">
|
|
508
|
+
{activeTab === "manifests" && (
|
|
509
|
+
<ManifestList
|
|
510
|
+
manifests={manifests}
|
|
511
|
+
selectedIndex={selectedManifestIndex}
|
|
512
|
+
loading={manifestsLoading}
|
|
513
|
+
/>
|
|
514
|
+
)}
|
|
515
|
+
{activeTab === "alerts" && (
|
|
516
|
+
<AlertList
|
|
517
|
+
alerts={alerts}
|
|
518
|
+
selectedIndex={selectedAlertIndex}
|
|
519
|
+
loading={alertsLoading}
|
|
520
|
+
/>
|
|
521
|
+
)}
|
|
522
|
+
{activeTab === "credentials" && (
|
|
523
|
+
<CredentialList
|
|
524
|
+
credentials={credentials}
|
|
525
|
+
loading={credentialsLoading}
|
|
526
|
+
/>
|
|
527
|
+
)}
|
|
528
|
+
{activeTab === "fraud" && (
|
|
529
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
530
|
+
<box flexGrow={1} width="100%">
|
|
531
|
+
<FraudScoreView
|
|
532
|
+
scores={fraudScores}
|
|
533
|
+
selectedIndex={selectedFraudIndex}
|
|
534
|
+
loading={fraudScoresLoading}
|
|
535
|
+
/>
|
|
536
|
+
</box>
|
|
537
|
+
<box flexDirection="column" width="100%">
|
|
538
|
+
<box height={1} width="100%">
|
|
539
|
+
<text>{"--- Collusion Rings ---"}</text>
|
|
540
|
+
</box>
|
|
541
|
+
{collusionLoading ? (
|
|
542
|
+
<box height={1} width="100%">
|
|
543
|
+
<text>Loading collusion rings...</text>
|
|
544
|
+
</box>
|
|
545
|
+
) : (collusionRings as { confidence: number; members: string[]; ring_type?: string }[]).length === 0 ? (
|
|
546
|
+
<box height={1} width="100%">
|
|
547
|
+
<text style={textStyle({ dim: true })}>No collusion rings detected</text>
|
|
548
|
+
</box>
|
|
549
|
+
) : (
|
|
550
|
+
(collusionRings as { confidence: number; members: string[]; ring_type?: string }[]).map((ring, i) => {
|
|
551
|
+
const conf = ring.confidence;
|
|
552
|
+
const confColor = conf > 0.7 ? statusColor.error : conf >= 0.4 ? statusColor.warning : undefined;
|
|
553
|
+
const confStr = conf.toFixed(3);
|
|
554
|
+
const members = ring.members.join(", ");
|
|
555
|
+
const ringType = ring.ring_type ?? "unknown";
|
|
556
|
+
return (
|
|
557
|
+
<box key={`ring-${i}`} height={1} width="100%">
|
|
558
|
+
<text>
|
|
559
|
+
{" "}
|
|
560
|
+
<span style={textStyle({ fg: confColor, dim: conf < 0.4 })}>{confStr}</span>
|
|
561
|
+
{` [${ringType}] ${members}`}
|
|
562
|
+
</text>
|
|
563
|
+
</box>
|
|
564
|
+
);
|
|
565
|
+
})
|
|
566
|
+
)}
|
|
567
|
+
</box>
|
|
568
|
+
<box flexDirection="column" width="100%">
|
|
569
|
+
<box height={1} width="100%">
|
|
570
|
+
<text>{"--- Governance Constraints ---"}</text>
|
|
571
|
+
</box>
|
|
572
|
+
<ConstraintList
|
|
573
|
+
constraints={constraints}
|
|
574
|
+
selectedIndex={selectedConstraintIndex}
|
|
575
|
+
loading={constraintsLoading}
|
|
576
|
+
/>
|
|
577
|
+
</box>
|
|
578
|
+
</box>
|
|
579
|
+
)}
|
|
580
|
+
{activeTab === "delegations" && (
|
|
581
|
+
<DelegationList
|
|
582
|
+
delegations={delegations}
|
|
583
|
+
selectedIndex={selectedDelegationIndex}
|
|
584
|
+
loading={delegationsLoading}
|
|
585
|
+
/>
|
|
586
|
+
)}
|
|
587
|
+
</box>
|
|
588
|
+
|
|
589
|
+
{/* Help bar */}
|
|
590
|
+
<box height={1} width="100%">
|
|
591
|
+
{copied
|
|
592
|
+
? <text style={textStyle({ fg: "green" })}>Copied!</text>
|
|
593
|
+
: <text>{HELP[activeTab]}</text>}
|
|
594
|
+
</box>
|
|
595
|
+
</box>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance alert list with severity icons and selection for resolve action.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { GovernanceAlert } from "../../stores/access-store.js";
|
|
7
|
+
import { EmptyState } from "../../shared/components/empty-state.js";
|
|
8
|
+
|
|
9
|
+
interface AlertListProps {
|
|
10
|
+
readonly alerts: readonly GovernanceAlert[];
|
|
11
|
+
readonly selectedIndex: number;
|
|
12
|
+
readonly loading: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SEVERITY_ICONS: Readonly<Record<GovernanceAlert["severity"], string>> = {
|
|
16
|
+
critical: "●",
|
|
17
|
+
warning: "◐",
|
|
18
|
+
info: "○",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function formatTimestamp(ts: string): string {
|
|
22
|
+
try {
|
|
23
|
+
return new Date(ts).toLocaleString();
|
|
24
|
+
} catch {
|
|
25
|
+
return ts;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function AlertList({ alerts, selectedIndex, loading }: AlertListProps): React.ReactNode {
|
|
30
|
+
if (loading) {
|
|
31
|
+
return (
|
|
32
|
+
<box height="100%" width="100%" justifyContent="center" alignItems="center">
|
|
33
|
+
<text>Loading alerts...</text>
|
|
34
|
+
</box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (alerts.length === 0) {
|
|
39
|
+
return <EmptyState message="No alerts found." />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<scrollbox height="100%" width="100%">
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<box height={1} width="100%">
|
|
46
|
+
<text>{" SEV TYPE AGENT DETAILS STATUS TIME"}</text>
|
|
47
|
+
</box>
|
|
48
|
+
<box height={1} width="100%">
|
|
49
|
+
<text>{" --- --------------- --------------- ------------------------------------- --------- ----"}</text>
|
|
50
|
+
</box>
|
|
51
|
+
|
|
52
|
+
{/* Rows */}
|
|
53
|
+
{alerts.map((alert, i) => {
|
|
54
|
+
const isSelected = i === selectedIndex;
|
|
55
|
+
const prefix = isSelected ? "> " : " ";
|
|
56
|
+
const icon = SEVERITY_ICONS[alert.severity] ?? "?";
|
|
57
|
+
const agent = alert.agent_id ?? "system";
|
|
58
|
+
const detailStr = typeof alert.details === "string"
|
|
59
|
+
? alert.details
|
|
60
|
+
: JSON.stringify(alert.details ?? "");
|
|
61
|
+
const details = detailStr.length > 37
|
|
62
|
+
? `${detailStr.slice(0, 34)}...`
|
|
63
|
+
: detailStr;
|
|
64
|
+
const status = alert.resolved ? "resolved" : "active";
|
|
65
|
+
const time = alert.created_at ? formatTimestamp(alert.created_at) : "-";
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<box key={alert.alert_id} height={1} width="100%">
|
|
69
|
+
<text>
|
|
70
|
+
{`${prefix}${icon} ${alert.alert_type.padEnd(15)} ${agent.padEnd(15)} ${details.padEnd(37)} ${status.padEnd(9)} ${time}`}
|
|
71
|
+
</text>
|
|
72
|
+
</box>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</scrollbox>
|
|
76
|
+
);
|
|
77
|
+
}
|