@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,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure & Events panel.
|
|
3
|
+
*
|
|
4
|
+
* Tabbed layout: Events (SSE stream) | Connectors | Subscriptions | Locks | Secrets
|
|
5
|
+
*
|
|
6
|
+
* Press 'f' to enter event type filter mode, 's' to enter search filter mode.
|
|
7
|
+
* In filter mode, type the filter value, Enter to apply, Escape to cancel.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
11
|
+
import { useEventsStore } from "../../stores/events-store.js";
|
|
12
|
+
import { useInfraStore } from "../../stores/infra-store.js";
|
|
13
|
+
import { useGlobalStore } from "../../stores/global-store.js";
|
|
14
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
15
|
+
import { useCopy } from "../../shared/hooks/use-copy.js";
|
|
16
|
+
import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
|
|
17
|
+
import { useApi } from "../../shared/hooks/use-api.js";
|
|
18
|
+
import { useUiStore } from "../../stores/ui-store.js";
|
|
19
|
+
import { useVisibleTabs } from "../../shared/hooks/use-visible-tabs.js";
|
|
20
|
+
import { ConnectorList } from "./connector-list.js";
|
|
21
|
+
import { ConnectorDetail } from "./connector-detail.js";
|
|
22
|
+
import { SubscriptionList } from "./subscription-list.js";
|
|
23
|
+
import { LockList } from "./lock-list.js";
|
|
24
|
+
import { SecretsAudit } from "./secrets-audit.js";
|
|
25
|
+
import { MclReplay } from "./mcl-replay.js";
|
|
26
|
+
import { EventReplay } from "./event-replay.js";
|
|
27
|
+
import { OperationsTab } from "./operations-tab.js";
|
|
28
|
+
import { AuditTrail } from "./audit-trail.js";
|
|
29
|
+
import { useKnowledgeStore } from "../../stores/knowledge-store.js";
|
|
30
|
+
import { EmptyState } from "../../shared/components/empty-state.js";
|
|
31
|
+
import { ScrollIndicator } from "../../shared/components/scroll-indicator.js";
|
|
32
|
+
import { Tooltip } from "../../shared/components/tooltip.js";
|
|
33
|
+
import { SubTabBar } from "../../shared/components/sub-tab-bar.js";
|
|
34
|
+
import { useTabFallback } from "../../shared/hooks/use-tab-fallback.js";
|
|
35
|
+
import { EVENTS_TABS } from "../../shared/navigation.js";
|
|
36
|
+
import { statusColor } from "../../shared/theme.js";
|
|
37
|
+
import {
|
|
38
|
+
type FilterMode,
|
|
39
|
+
type EventsBindingContext,
|
|
40
|
+
getEventsKeyBindings,
|
|
41
|
+
getEventsHelpText,
|
|
42
|
+
handleEventsUnhandledKey,
|
|
43
|
+
formatEventData,
|
|
44
|
+
} from "./events-panel-keybindings.js";
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export default function EventsPanel(): React.ReactNode {
|
|
48
|
+
const apiClient = useApi();
|
|
49
|
+
const confirm = useConfirmStore((s) => s.confirm);
|
|
50
|
+
const overlayActive = useUiStore((s) => s.overlayActive);
|
|
51
|
+
const visibleTabs = useVisibleTabs(EVENTS_TABS);
|
|
52
|
+
const config = useGlobalStore((s) => s.config);
|
|
53
|
+
|
|
54
|
+
// Clipboard copy
|
|
55
|
+
const { copy, copied } = useCopy();
|
|
56
|
+
|
|
57
|
+
// Filter input state
|
|
58
|
+
const [filterMode, setFilterMode] = useState<FilterMode>("none");
|
|
59
|
+
const [filterBuffer, setFilterBuffer] = useState("");
|
|
60
|
+
|
|
61
|
+
// Event detail expansion
|
|
62
|
+
const [selectedEventIndex, setSelectedEventIndex] = useState(-1);
|
|
63
|
+
const [expandedEventIndex, setExpandedEventIndex] = useState<number | null>(null);
|
|
64
|
+
|
|
65
|
+
// MCL filter state
|
|
66
|
+
const [mclUrnFilter, setMclUrnFilter] = useState("");
|
|
67
|
+
const [mclAspectFilter, setMclAspectFilter] = useState("");
|
|
68
|
+
|
|
69
|
+
// Secrets filter state
|
|
70
|
+
const [secretsFilter, setSecretsFilter] = useState("");
|
|
71
|
+
|
|
72
|
+
// Replay filter state
|
|
73
|
+
const [replayTypeFilter, setReplayTypeFilter] = useState("");
|
|
74
|
+
|
|
75
|
+
// Connector detail state
|
|
76
|
+
const [connectorDetailView, setConnectorDetailView] = useState(false);
|
|
77
|
+
|
|
78
|
+
// Audit selected index
|
|
79
|
+
const [selectedAuditIndex, setSelectedAuditIndex] = useState(0);
|
|
80
|
+
|
|
81
|
+
// Events store (SSE)
|
|
82
|
+
const connected = useEventsStore((s) => s.connected);
|
|
83
|
+
const events = useEventsStore((s) => s.filteredEvents);
|
|
84
|
+
const reconnectCount = useEventsStore((s) => s.reconnectCount);
|
|
85
|
+
const reconnectExhausted = useEventsStore((s) => s.reconnectExhausted);
|
|
86
|
+
const filters = useEventsStore((s) => s.filters);
|
|
87
|
+
const eventsOverflowed = useEventsStore((s) => s.eventsOverflowed);
|
|
88
|
+
const evictedCount = useEventsStore((s) => s.evictedCount);
|
|
89
|
+
const eventsBuffer = useEventsStore((s) => s.eventsBuffer);
|
|
90
|
+
const connect = useEventsStore((s) => s.connect);
|
|
91
|
+
const disconnect = useEventsStore((s) => s.disconnect);
|
|
92
|
+
const clearEvents = useEventsStore((s) => s.clearEvents);
|
|
93
|
+
const setFilter = useEventsStore((s) => s.setFilter);
|
|
94
|
+
|
|
95
|
+
// Infra store
|
|
96
|
+
const infraTab = useInfraStore((s) => s.activeTab);
|
|
97
|
+
const connectors = useInfraStore((s) => s.connectors);
|
|
98
|
+
const connectorsLoading = useInfraStore((s) => s.connectorsLoading);
|
|
99
|
+
const selectedConnectorIndex = useInfraStore((s) => s.selectedConnectorIndex);
|
|
100
|
+
const subscriptions = useInfraStore((s) => s.subscriptions);
|
|
101
|
+
const subscriptionsLoading = useInfraStore((s) => s.subscriptionsLoading);
|
|
102
|
+
const selectedSubscriptionIndex = useInfraStore((s) => s.selectedSubscriptionIndex);
|
|
103
|
+
const locks = useInfraStore((s) => s.locks);
|
|
104
|
+
const locksLoading = useInfraStore((s) => s.locksLoading);
|
|
105
|
+
const selectedLockIndex = useInfraStore((s) => s.selectedLockIndex);
|
|
106
|
+
const secretAuditEntries = useInfraStore((s) => s.secretAuditEntries);
|
|
107
|
+
const secretsLoading = useInfraStore((s) => s.secretsLoading);
|
|
108
|
+
const operations = useInfraStore((s) => s.operations);
|
|
109
|
+
const operationsLoading = useInfraStore((s) => s.operationsLoading);
|
|
110
|
+
const selectedOperationIndex = useInfraStore((s) => s.selectedOperationIndex);
|
|
111
|
+
const infraError = useInfraStore((s) => s.error);
|
|
112
|
+
|
|
113
|
+
const connectorCapabilities = useInfraStore((s) => s.connectorCapabilities);
|
|
114
|
+
const capabilitiesLoading = useInfraStore((s) => s.capabilitiesLoading);
|
|
115
|
+
const auditTransactions = useInfraStore((s) => s.auditTransactions);
|
|
116
|
+
const auditLoading = useInfraStore((s) => s.auditLoading);
|
|
117
|
+
const auditHasMore = useInfraStore((s) => s.auditHasMore);
|
|
118
|
+
const auditNextCursor = useInfraStore((s) => s.auditNextCursor);
|
|
119
|
+
|
|
120
|
+
const fetchConnectors = useInfraStore((s) => s.fetchConnectors);
|
|
121
|
+
const fetchSubscriptions = useInfraStore((s) => s.fetchSubscriptions);
|
|
122
|
+
const deleteSubscription = useInfraStore((s) => s.deleteSubscription);
|
|
123
|
+
const testSubscription = useInfraStore((s) => s.testSubscription);
|
|
124
|
+
const fetchLocks = useInfraStore((s) => s.fetchLocks);
|
|
125
|
+
const acquireLock = useInfraStore((s) => s.acquireLock);
|
|
126
|
+
const releaseLock = useInfraStore((s) => s.releaseLock);
|
|
127
|
+
const extendLock = useInfraStore((s) => s.extendLock);
|
|
128
|
+
const fetchSecretAudit = useInfraStore((s) => s.fetchSecretAudit);
|
|
129
|
+
const fetchOperations = useInfraStore((s) => s.fetchOperations);
|
|
130
|
+
const fetchConnectorCapabilities = useInfraStore((s) => s.fetchConnectorCapabilities);
|
|
131
|
+
const fetchAuditTransactions = useInfraStore((s) => s.fetchAuditTransactions);
|
|
132
|
+
const setSelectedOperationIndex = useInfraStore((s) => s.setSelectedOperationIndex);
|
|
133
|
+
const activeTab = useInfraStore((s) => s.activePanelTab);
|
|
134
|
+
const setActiveTab = useInfraStore((s) => s.setActivePanelTab);
|
|
135
|
+
const setSelectedConnectorIndex = useInfraStore((s) => s.setSelectedConnectorIndex);
|
|
136
|
+
const setSelectedSubscriptionIndex = useInfraStore((s) => s.setSelectedSubscriptionIndex);
|
|
137
|
+
const setSelectedLockIndex = useInfraStore((s) => s.setSelectedLockIndex);
|
|
138
|
+
|
|
139
|
+
useTabFallback(visibleTabs, activeTab, setActiveTab);
|
|
140
|
+
|
|
141
|
+
// Reset expanded event when events change (index may become stale after SSE adds/evicts)
|
|
142
|
+
const eventsLength = events.length;
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
setExpandedEventIndex(null);
|
|
145
|
+
}, [eventsLength]);
|
|
146
|
+
|
|
147
|
+
// Auto-connect SSE on mount, reconnect when identity changes
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (config.apiKey && config.baseUrl) {
|
|
150
|
+
connect(config.baseUrl, config.apiKey, {
|
|
151
|
+
agentId: config.agentId,
|
|
152
|
+
subject: config.subject,
|
|
153
|
+
zoneId: config.zoneId,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return () => disconnect();
|
|
157
|
+
}, [config.apiKey, config.baseUrl, config.agentId, config.subject, config.zoneId, connect, disconnect]);
|
|
158
|
+
|
|
159
|
+
// Knowledge store (MCL replay + event replay)
|
|
160
|
+
const fetchReplay = useKnowledgeStore((s) => s.fetchReplay);
|
|
161
|
+
const clearReplay = useKnowledgeStore((s) => s.clearReplay);
|
|
162
|
+
const fetchEventReplay = useKnowledgeStore((s) => s.fetchEventReplay);
|
|
163
|
+
const clearEventReplay = useKnowledgeStore((s) => s.clearEventReplay);
|
|
164
|
+
|
|
165
|
+
// Fetch infra data when switching tabs
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!apiClient || activeTab === "events") return;
|
|
168
|
+
|
|
169
|
+
if (activeTab === "mcl") void fetchReplay(apiClient, 0, 50);
|
|
170
|
+
else if (activeTab === "replay") void fetchEventReplay({}, apiClient);
|
|
171
|
+
else if (activeTab === "connectors") { fetchConnectors(apiClient); setConnectorDetailView(false); }
|
|
172
|
+
else if (activeTab === "subscriptions") fetchSubscriptions(apiClient);
|
|
173
|
+
else if (activeTab === "locks") fetchLocks(apiClient);
|
|
174
|
+
else if (activeTab === "secrets") fetchSecretAudit(apiClient);
|
|
175
|
+
else if (activeTab === "operations") fetchOperations(apiClient);
|
|
176
|
+
else if (activeTab === "audit") void fetchAuditTransactions({}, apiClient);
|
|
177
|
+
}, [activeTab, apiClient, fetchConnectors, fetchSubscriptions, fetchLocks, fetchSecretAudit, fetchOperations, fetchReplay, fetchEventReplay, fetchAuditTransactions]);
|
|
178
|
+
|
|
179
|
+
// Build binding context for keybinding builders (Decision 6A)
|
|
180
|
+
const bindingCtx: EventsBindingContext = {
|
|
181
|
+
activeTab, visibleTabs, setActiveTab,
|
|
182
|
+
filterMode, filterBuffer, setFilterMode, setFilterBuffer,
|
|
183
|
+
events, selectedEventIndex, setSelectedEventIndex,
|
|
184
|
+
expandedEventIndex, setExpandedEventIndex,
|
|
185
|
+
filters, setFilter, clearEvents, copy,
|
|
186
|
+
config, disconnect, connect,
|
|
187
|
+
mclUrnFilter, setMclUrnFilter, mclAspectFilter, setMclAspectFilter,
|
|
188
|
+
clearReplay, fetchReplay,
|
|
189
|
+
replayTypeFilter, setReplayTypeFilter, clearEventReplay, fetchEventReplay,
|
|
190
|
+
connectors, selectedConnectorIndex, setSelectedConnectorIndex,
|
|
191
|
+
connectorDetailView, setConnectorDetailView,
|
|
192
|
+
fetchConnectors, fetchConnectorCapabilities,
|
|
193
|
+
subscriptions, selectedSubscriptionIndex, setSelectedSubscriptionIndex,
|
|
194
|
+
deleteSubscription, testSubscription, fetchSubscriptions,
|
|
195
|
+
locks, selectedLockIndex, setSelectedLockIndex,
|
|
196
|
+
acquireLock, releaseLock, extendLock, fetchLocks,
|
|
197
|
+
secretsFilter, setSecretsFilter, fetchSecretAudit,
|
|
198
|
+
operations, selectedOperationIndex, setSelectedOperationIndex, fetchOperations,
|
|
199
|
+
auditTransactions, selectedAuditIndex, setSelectedAuditIndex,
|
|
200
|
+
auditHasMore, auditNextCursor, fetchAuditTransactions,
|
|
201
|
+
apiClient, confirm,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Handle unhandled keys in filter input mode
|
|
205
|
+
const handleUnhandledKey = useCallback(
|
|
206
|
+
(keyName: string) => handleEventsUnhandledKey(filterMode, setFilterBuffer, keyName),
|
|
207
|
+
[filterMode],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
useKeyboard(
|
|
211
|
+
getEventsKeyBindings(overlayActive, bindingCtx),
|
|
212
|
+
overlayActive ? undefined : handleUnhandledKey,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
217
|
+
<Tooltip tooltipKey="events-panel" message="Tip: Press ? for keybinding help" />
|
|
218
|
+
{/* Tab bar */}
|
|
219
|
+
<SubTabBar tabs={visibleTabs} activeTab={activeTab} />
|
|
220
|
+
|
|
221
|
+
{/* Filter bar (events tab) */}
|
|
222
|
+
{activeTab === "events" && (
|
|
223
|
+
<box height={1} width="100%">
|
|
224
|
+
<text>
|
|
225
|
+
{filterMode === "type"
|
|
226
|
+
? `Filter type: ${filterBuffer}\u2588`
|
|
227
|
+
: filterMode === "search"
|
|
228
|
+
? `Filter search: ${filterBuffer}\u2588`
|
|
229
|
+
: `Filter: type=${filters.eventType ?? "*"} search=${filters.search ?? "*"}`}
|
|
230
|
+
</text>
|
|
231
|
+
</box>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* Filter bar (MCL tab) */}
|
|
235
|
+
{activeTab === "mcl" && (
|
|
236
|
+
<box height={1} width="100%">
|
|
237
|
+
<text>
|
|
238
|
+
{filterMode === "mcl_urn"
|
|
239
|
+
? `Filter URN: ${filterBuffer}\u2588`
|
|
240
|
+
: filterMode === "mcl_aspect"
|
|
241
|
+
? `Filter aspect: ${filterBuffer}\u2588`
|
|
242
|
+
: `Filter: URN=${mclUrnFilter || "*"} aspect=${mclAspectFilter || "*"}`}
|
|
243
|
+
</text>
|
|
244
|
+
</box>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{/* Acquire lock input bar (locks tab) */}
|
|
248
|
+
{activeTab === "locks" && filterMode === "acquire_path" && (
|
|
249
|
+
<box height={1} width="100%">
|
|
250
|
+
<text>{`Acquire lock path: ${filterBuffer}\u2588`}</text>
|
|
251
|
+
</box>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* Replay filter bar */}
|
|
255
|
+
{activeTab === "replay" && (
|
|
256
|
+
<box height={1} width="100%">
|
|
257
|
+
<text>
|
|
258
|
+
{filterMode === "replay_filter"
|
|
259
|
+
? `Filter event type: ${filterBuffer}\u2588`
|
|
260
|
+
: `Filter: event_type=${replayTypeFilter || "*"}`}
|
|
261
|
+
</text>
|
|
262
|
+
</box>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Secrets filter bar */}
|
|
266
|
+
{activeTab === "secrets" && (
|
|
267
|
+
<box height={1} width="100%">
|
|
268
|
+
<text>
|
|
269
|
+
{filterMode === "secrets_filter"
|
|
270
|
+
? `Filter: ${filterBuffer}\u2588`
|
|
271
|
+
: secretsFilter
|
|
272
|
+
? `Filter: ${secretsFilter}`
|
|
273
|
+
: ""}
|
|
274
|
+
</text>
|
|
275
|
+
</box>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Error display */}
|
|
279
|
+
{infraError && activeTab !== "events" && activeTab !== "mcl" && activeTab !== "replay" && (
|
|
280
|
+
<box height={1} width="100%">
|
|
281
|
+
<text>{`Error: ${infraError}`}</text>
|
|
282
|
+
</box>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* Main content */}
|
|
286
|
+
<box flexGrow={1} width="100%" borderStyle="single">
|
|
287
|
+
{activeTab === "events" && (
|
|
288
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
289
|
+
{/* SSE status */}
|
|
290
|
+
<box height={1} width="100%">
|
|
291
|
+
<text>
|
|
292
|
+
{connected
|
|
293
|
+
? `● Connected — ${events.length} events`
|
|
294
|
+
: reconnectExhausted
|
|
295
|
+
? `✕ Reconnect failed after ${reconnectCount} attempts — press r to retry`
|
|
296
|
+
: reconnectCount > 0
|
|
297
|
+
? `◐ Auto-reconnecting (attempt ${reconnectCount}/10)...`
|
|
298
|
+
: "○ Disconnected"}
|
|
299
|
+
</text>
|
|
300
|
+
</box>
|
|
301
|
+
|
|
302
|
+
{/* Overflow indicator */}
|
|
303
|
+
{eventsOverflowed && (
|
|
304
|
+
<box height={1} width="100%">
|
|
305
|
+
<text dimColor>
|
|
306
|
+
{`Showing latest ${eventsBuffer.size} of ${eventsBuffer.totalAdded} events (${evictedCount} evicted)`}
|
|
307
|
+
</text>
|
|
308
|
+
</box>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
{/* Event stream */}
|
|
312
|
+
{expandedEventIndex !== null && expandedEventIndex < events.length ? (
|
|
313
|
+
<box flexGrow={1} width="100%" flexDirection="column">
|
|
314
|
+
<box height={1} width="100%">
|
|
315
|
+
<text bold>{`[${events[expandedEventIndex]!.event}] — Event #${expandedEventIndex} (Escape to close)`}</text>
|
|
316
|
+
</box>
|
|
317
|
+
<scrollbox flexGrow={1} width="100%">
|
|
318
|
+
<text>{formatEventData(events[expandedEventIndex]!.data)}</text>
|
|
319
|
+
</scrollbox>
|
|
320
|
+
</box>
|
|
321
|
+
) : (
|
|
322
|
+
<ScrollIndicator selectedIndex={selectedEventIndex >= 0 ? selectedEventIndex : events.length - 1} totalItems={events.length} visibleItems={20}>
|
|
323
|
+
<scrollbox flexGrow={1} width="100%">
|
|
324
|
+
{events.length === 0 ? (
|
|
325
|
+
<EmptyState
|
|
326
|
+
message="Listening for events..."
|
|
327
|
+
hint="Waiting for activity on the server."
|
|
328
|
+
/>
|
|
329
|
+
) : (
|
|
330
|
+
events.map((event, index) => (
|
|
331
|
+
<box key={event.id ?? index} height={1} width="100%" flexDirection="row">
|
|
332
|
+
<text inverse={index === selectedEventIndex || undefined}>
|
|
333
|
+
{`[${event.event}] ${event.data}`}
|
|
334
|
+
</text>
|
|
335
|
+
</box>
|
|
336
|
+
))
|
|
337
|
+
)}
|
|
338
|
+
</scrollbox>
|
|
339
|
+
</ScrollIndicator>
|
|
340
|
+
)}
|
|
341
|
+
</box>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{activeTab === "mcl" && <MclReplay urnFilter={mclUrnFilter} aspectFilter={mclAspectFilter} />}
|
|
345
|
+
|
|
346
|
+
{activeTab === "replay" && <EventReplay typeFilter={replayTypeFilter} />}
|
|
347
|
+
|
|
348
|
+
{activeTab === "connectors" && (
|
|
349
|
+
connectorDetailView && connectors[selectedConnectorIndex] ? (
|
|
350
|
+
<ConnectorDetail
|
|
351
|
+
connectorName={connectors[selectedConnectorIndex]!.name}
|
|
352
|
+
capabilities={connectorCapabilities}
|
|
353
|
+
loading={capabilitiesLoading}
|
|
354
|
+
/>
|
|
355
|
+
) : (
|
|
356
|
+
<ConnectorList
|
|
357
|
+
connectors={connectors}
|
|
358
|
+
selectedIndex={selectedConnectorIndex}
|
|
359
|
+
loading={connectorsLoading}
|
|
360
|
+
/>
|
|
361
|
+
)
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{activeTab === "subscriptions" && (
|
|
365
|
+
<SubscriptionList
|
|
366
|
+
subscriptions={subscriptions}
|
|
367
|
+
selectedIndex={selectedSubscriptionIndex}
|
|
368
|
+
loading={subscriptionsLoading}
|
|
369
|
+
/>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{activeTab === "locks" && (
|
|
373
|
+
<LockList
|
|
374
|
+
locks={locks}
|
|
375
|
+
selectedIndex={selectedLockIndex}
|
|
376
|
+
loading={locksLoading}
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{activeTab === "secrets" && (
|
|
381
|
+
<SecretsAudit
|
|
382
|
+
entries={secretAuditEntries}
|
|
383
|
+
loading={secretsLoading}
|
|
384
|
+
filter={secretsFilter}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{activeTab === "operations" && (
|
|
389
|
+
<OperationsTab
|
|
390
|
+
operations={operations}
|
|
391
|
+
selectedIndex={selectedOperationIndex}
|
|
392
|
+
loading={operationsLoading}
|
|
393
|
+
/>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{activeTab === "audit" && (
|
|
397
|
+
<AuditTrail
|
|
398
|
+
transactions={auditTransactions}
|
|
399
|
+
loading={auditLoading}
|
|
400
|
+
hasMore={auditHasMore}
|
|
401
|
+
selectedIndex={selectedAuditIndex}
|
|
402
|
+
/>
|
|
403
|
+
)}
|
|
404
|
+
</box>
|
|
405
|
+
|
|
406
|
+
{/* Help bar */}
|
|
407
|
+
<box height={1} width="100%">
|
|
408
|
+
{copied
|
|
409
|
+
? <text foregroundColor={statusColor.healthy}>Copied!</text>
|
|
410
|
+
: <text>{getEventsHelpText(filterMode, activeTab, connectorDetailView)}</text>}
|
|
411
|
+
</box>
|
|
412
|
+
</box>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events SSE stream tab: live event stream with connection states,
|
|
3
|
+
* filtering, expansion, and copy support.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from events-panel.tsx (Issue 2A: split into per-tab sub-panels).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect } from "react";
|
|
9
|
+
import { useEventsStore } from "../../stores/events-store.js";
|
|
10
|
+
import { useGlobalStore } from "../../stores/global-store.js";
|
|
11
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
12
|
+
import { useCopy } from "../../shared/hooks/use-copy.js";
|
|
13
|
+
import { useTextInput } from "../../shared/hooks/use-text-input.js";
|
|
14
|
+
import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
|
|
15
|
+
import { statusColor } from "../../shared/theme.js";
|
|
16
|
+
import { EmptyState } from "../../shared/components/empty-state.js";
|
|
17
|
+
import { ScrollIndicator } from "../../shared/components/scroll-indicator.js";
|
|
18
|
+
|
|
19
|
+
function formatEventData(data: string): string {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(data);
|
|
22
|
+
return JSON.stringify(parsed, null, 2);
|
|
23
|
+
} catch {
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HELP_NORMAL = "j/k:navigate Enter:expand f:filter type s:search c:clear r:reconnect y:copy Tab:switch";
|
|
29
|
+
const HELP_INPUT = "Type value, Enter:apply, Escape:cancel, Backspace:delete";
|
|
30
|
+
|
|
31
|
+
interface EventsTabProps {
|
|
32
|
+
/** Tab-level keybindings (tab cycling) to merge. */
|
|
33
|
+
readonly tabBindings: Readonly<Record<string, () => void>>;
|
|
34
|
+
readonly overlayActive: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function EventsTab({ tabBindings, overlayActive }: EventsTabProps): React.ReactNode {
|
|
38
|
+
const config = useGlobalStore((s) => s.config);
|
|
39
|
+
const { copy, copied } = useCopy();
|
|
40
|
+
|
|
41
|
+
// SSE state
|
|
42
|
+
const connected = useEventsStore((s) => s.connected);
|
|
43
|
+
const events = useEventsStore((s) => s.filteredEvents);
|
|
44
|
+
const reconnectCount = useEventsStore((s) => s.reconnectCount);
|
|
45
|
+
const reconnectExhausted = useEventsStore((s) => s.reconnectExhausted);
|
|
46
|
+
const filters = useEventsStore((s) => s.filters);
|
|
47
|
+
const eventsOverflowed = useEventsStore((s) => s.eventsOverflowed);
|
|
48
|
+
const evictedCount = useEventsStore((s) => s.evictedCount);
|
|
49
|
+
const eventsBuffer = useEventsStore((s) => s.eventsBuffer);
|
|
50
|
+
const connect = useEventsStore((s) => s.connect);
|
|
51
|
+
const disconnect = useEventsStore((s) => s.disconnect);
|
|
52
|
+
const clearEvents = useEventsStore((s) => s.clearEvents);
|
|
53
|
+
const setFilter = useEventsStore((s) => s.setFilter);
|
|
54
|
+
|
|
55
|
+
// Selection and expansion
|
|
56
|
+
const [selectedEventIndex, setSelectedEventIndex] = useState(-1);
|
|
57
|
+
const [expandedEventIndex, setExpandedEventIndex] = useState<number | null>(null);
|
|
58
|
+
|
|
59
|
+
// Reset expanded event when events change (index may become stale)
|
|
60
|
+
const eventsLength = events.length;
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setExpandedEventIndex(null);
|
|
63
|
+
}, [eventsLength]);
|
|
64
|
+
|
|
65
|
+
// Auto-connect SSE on mount
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (config.apiKey && config.baseUrl) {
|
|
68
|
+
connect(config.baseUrl, config.apiKey, {
|
|
69
|
+
agentId: config.agentId,
|
|
70
|
+
subject: config.subject,
|
|
71
|
+
zoneId: config.zoneId,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return () => disconnect();
|
|
75
|
+
}, [config.apiKey, config.baseUrl, config.agentId, config.subject, config.zoneId, connect, disconnect]);
|
|
76
|
+
|
|
77
|
+
// Text inputs for type and search filters
|
|
78
|
+
const typeFilter = useTextInput({
|
|
79
|
+
onSubmit: (val) => setFilter({ eventType: val || null }),
|
|
80
|
+
});
|
|
81
|
+
const searchFilter = useTextInput({
|
|
82
|
+
onSubmit: (val) => setFilter({ search: val || null }),
|
|
83
|
+
});
|
|
84
|
+
const anyFilterActive = typeFilter.active || searchFilter.active;
|
|
85
|
+
|
|
86
|
+
// List navigation
|
|
87
|
+
const listNav = listNavigationBindings({
|
|
88
|
+
getIndex: () => selectedEventIndex,
|
|
89
|
+
setIndex: (i) => setSelectedEventIndex(i),
|
|
90
|
+
getLength: () => events.length,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
useKeyboard(
|
|
94
|
+
overlayActive
|
|
95
|
+
? {}
|
|
96
|
+
: anyFilterActive
|
|
97
|
+
? (typeFilter.active ? typeFilter.inputBindings : searchFilter.inputBindings)
|
|
98
|
+
: {
|
|
99
|
+
...listNav,
|
|
100
|
+
...tabBindings,
|
|
101
|
+
return: () => {
|
|
102
|
+
if (selectedEventIndex >= 0 && selectedEventIndex < events.length) {
|
|
103
|
+
setExpandedEventIndex((prev) => prev === selectedEventIndex ? null : selectedEventIndex);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
escape: () => {
|
|
107
|
+
if (expandedEventIndex !== null) setExpandedEventIndex(null);
|
|
108
|
+
},
|
|
109
|
+
c: () => clearEvents(),
|
|
110
|
+
r: () => {
|
|
111
|
+
if (config.apiKey && config.baseUrl) {
|
|
112
|
+
disconnect();
|
|
113
|
+
connect(config.baseUrl, config.apiKey, {
|
|
114
|
+
agentId: config.agentId,
|
|
115
|
+
subject: config.subject,
|
|
116
|
+
zoneId: config.zoneId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
f: () => typeFilter.activate(filters.eventType ?? ""),
|
|
121
|
+
s: () => searchFilter.activate(filters.search ?? ""),
|
|
122
|
+
y: () => {
|
|
123
|
+
const idx = selectedEventIndex >= 0 ? selectedEventIndex : events.length - 1;
|
|
124
|
+
const event = events[idx];
|
|
125
|
+
if (event) copy(event.data);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
overlayActive ? undefined : anyFilterActive
|
|
129
|
+
? (typeFilter.active ? typeFilter.onUnhandled : searchFilter.onUnhandled)
|
|
130
|
+
: undefined,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
135
|
+
{/* Filter bar */}
|
|
136
|
+
<box height={1} width="100%">
|
|
137
|
+
<text>
|
|
138
|
+
{typeFilter.active
|
|
139
|
+
? `Filter type: ${typeFilter.buffer}\u2588`
|
|
140
|
+
: searchFilter.active
|
|
141
|
+
? `Filter search: ${searchFilter.buffer}\u2588`
|
|
142
|
+
: `Filter: type=${filters.eventType ?? "*"} search=${filters.search ?? "*"}`}
|
|
143
|
+
</text>
|
|
144
|
+
</box>
|
|
145
|
+
|
|
146
|
+
{/* Main content */}
|
|
147
|
+
<box flexGrow={1} width="100%" borderStyle="single">
|
|
148
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
149
|
+
{/* SSE status */}
|
|
150
|
+
<box height={1} width="100%">
|
|
151
|
+
<text>
|
|
152
|
+
{connected
|
|
153
|
+
? `● Connected — ${events.length} events`
|
|
154
|
+
: reconnectExhausted
|
|
155
|
+
? `✕ Reconnect failed after ${reconnectCount} attempts — press r to retry`
|
|
156
|
+
: reconnectCount > 0
|
|
157
|
+
? `◐ Auto-reconnecting (attempt ${reconnectCount}/10)...`
|
|
158
|
+
: "○ Disconnected"}
|
|
159
|
+
</text>
|
|
160
|
+
</box>
|
|
161
|
+
|
|
162
|
+
{/* Overflow indicator */}
|
|
163
|
+
{eventsOverflowed && (
|
|
164
|
+
<box height={1} width="100%">
|
|
165
|
+
<text dimColor>
|
|
166
|
+
{`Showing latest ${eventsBuffer.size} of ${eventsBuffer.totalAdded} events (${evictedCount} evicted)`}
|
|
167
|
+
</text>
|
|
168
|
+
</box>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Event stream */}
|
|
172
|
+
{expandedEventIndex !== null && expandedEventIndex < events.length ? (
|
|
173
|
+
<box flexGrow={1} width="100%" flexDirection="column">
|
|
174
|
+
<box height={1} width="100%">
|
|
175
|
+
<text bold>{`[${events[expandedEventIndex]!.event}] — Event #${expandedEventIndex} (Escape to close)`}</text>
|
|
176
|
+
</box>
|
|
177
|
+
<scrollbox flexGrow={1} width="100%">
|
|
178
|
+
<text>{formatEventData(events[expandedEventIndex]!.data)}</text>
|
|
179
|
+
</scrollbox>
|
|
180
|
+
</box>
|
|
181
|
+
) : (
|
|
182
|
+
<ScrollIndicator selectedIndex={selectedEventIndex >= 0 ? selectedEventIndex : events.length - 1} totalItems={events.length} visibleItems={20}>
|
|
183
|
+
<scrollbox flexGrow={1} width="100%">
|
|
184
|
+
{events.length === 0 ? (
|
|
185
|
+
<EmptyState
|
|
186
|
+
message="Listening for events..."
|
|
187
|
+
hint="Waiting for activity on the server."
|
|
188
|
+
/>
|
|
189
|
+
) : (
|
|
190
|
+
events.map((event, index) => (
|
|
191
|
+
<box key={event.id ?? index} height={1} width="100%" flexDirection="row">
|
|
192
|
+
<text inverse={index === selectedEventIndex || undefined}>
|
|
193
|
+
{`[${event.event}] ${event.data}`}
|
|
194
|
+
</text>
|
|
195
|
+
</box>
|
|
196
|
+
))
|
|
197
|
+
)}
|
|
198
|
+
</scrollbox>
|
|
199
|
+
</ScrollIndicator>
|
|
200
|
+
)}
|
|
201
|
+
</box>
|
|
202
|
+
</box>
|
|
203
|
+
|
|
204
|
+
{/* Help bar */}
|
|
205
|
+
<box height={1} width="100%">
|
|
206
|
+
{copied
|
|
207
|
+
? <text foregroundColor={statusColor.success}>Copied!</text>
|
|
208
|
+
: <text>{anyFilterActive ? HELP_INPUT : HELP_NORMAL}</text>}
|
|
209
|
+
</box>
|
|
210
|
+
</box>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock list view: shows distributed locks with holder and TTL info.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { Lock } from "../../stores/infra-store.js";
|
|
7
|
+
import { Spinner } from "../../shared/components/spinner.js";
|
|
8
|
+
|
|
9
|
+
const MODE_ICON: Record<string, string> = {
|
|
10
|
+
mutex: "🔒",
|
|
11
|
+
semaphore: "🔗",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function LockList({
|
|
15
|
+
locks,
|
|
16
|
+
selectedIndex,
|
|
17
|
+
loading,
|
|
18
|
+
}: {
|
|
19
|
+
readonly locks: readonly Lock[];
|
|
20
|
+
readonly selectedIndex: number;
|
|
21
|
+
readonly loading: boolean;
|
|
22
|
+
}): React.ReactNode {
|
|
23
|
+
if (loading) {
|
|
24
|
+
return <Spinner label="Loading locks..." />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (locks.length === 0) {
|
|
28
|
+
return <text>No active locks</text>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<scrollbox height="100%" width="100%">
|
|
33
|
+
{/* Header */}
|
|
34
|
+
<box height={1} width="100%">
|
|
35
|
+
<text>{" Mode Resource Holder Fence Expires"}</text>
|
|
36
|
+
</box>
|
|
37
|
+
|
|
38
|
+
{locks.map((lock, i) => {
|
|
39
|
+
const prefix = i === selectedIndex ? "> " : " ";
|
|
40
|
+
const icon = MODE_ICON[lock.mode] ?? "?";
|
|
41
|
+
const resource = lock.resource.padEnd(24).slice(0, 24);
|
|
42
|
+
const holder = lock.holder_info.padEnd(20).slice(0, 20);
|
|
43
|
+
const fence = String(lock.fence_token).padStart(6);
|
|
44
|
+
const expires = new Date(lock.expires_at * 1000).toISOString().slice(11, 19);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<box key={lock.lock_id} height={1} width="100%">
|
|
48
|
+
<text>{`${prefix}${icon} ${resource} ${holder} ${fence} ${expires}`}</text>
|
|
49
|
+
</box>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</scrollbox>
|
|
53
|
+
);
|
|
54
|
+
}
|