@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows & Automation panel: tabbed layout with workflows, executions,
|
|
3
|
+
* and scheduler metrics views.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
7
|
+
import { useWorkflowsStore } from "../../stores/workflows-store.js";
|
|
8
|
+
import type { WorkflowTab } from "../../stores/workflows-store.js";
|
|
9
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
10
|
+
import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
|
|
11
|
+
import { useApi } from "../../shared/hooks/use-api.js";
|
|
12
|
+
import { useUiStore } from "../../stores/ui-store.js";
|
|
13
|
+
import { useVisibleTabs } from "../../shared/hooks/use-visible-tabs.js";
|
|
14
|
+
import { SubTabBar } from "../../shared/components/sub-tab-bar.js";
|
|
15
|
+
import { subTabCycleBindings } from "../../shared/components/sub-tab-bar-utils.js";
|
|
16
|
+
import { useTabFallback } from "../../shared/hooks/use-tab-fallback.js";
|
|
17
|
+
import { BrickGate } from "../../shared/components/brick-gate.js";
|
|
18
|
+
import { ConfirmDialog } from "../../shared/components/confirm-dialog.js";
|
|
19
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
20
|
+
import { WorkflowList } from "./workflow-list.js";
|
|
21
|
+
import { ExecutionList } from "./execution-list.js";
|
|
22
|
+
import { SchedulerView } from "./scheduler-view.js";
|
|
23
|
+
import { Tooltip } from "../../shared/components/tooltip.js";
|
|
24
|
+
import { textStyle } from "../../shared/text-style.js";
|
|
25
|
+
|
|
26
|
+
const HELP_TEXT: Readonly<Record<string, string>> = {
|
|
27
|
+
workflows: "j/k:navigate Tab:switch tab e:execute d:delete p:enable/disable r:refresh Enter:detail q:quit",
|
|
28
|
+
executions: "j/k:navigate Tab:switch tab Enter:detail Esc:close r:refresh q:quit",
|
|
29
|
+
scheduler: "Tab:switch tab r:refresh q:quit",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default function WorkflowsPanel(): React.ReactNode {
|
|
33
|
+
const client = useApi();
|
|
34
|
+
|
|
35
|
+
const workflows = useWorkflowsStore((s) => s.workflows);
|
|
36
|
+
const selectedWorkflowIndex = useWorkflowsStore((s) => s.selectedWorkflowIndex);
|
|
37
|
+
const workflowsLoading = useWorkflowsStore((s) => s.workflowsLoading);
|
|
38
|
+
const selectedWorkflow = useWorkflowsStore((s) => s.selectedWorkflow);
|
|
39
|
+
const detailLoading = useWorkflowsStore((s) => s.detailLoading);
|
|
40
|
+
const executions = useWorkflowsStore((s) => s.executions);
|
|
41
|
+
const selectedExecutionIndex = useWorkflowsStore((s) => s.selectedExecutionIndex);
|
|
42
|
+
const executionsLoading = useWorkflowsStore((s) => s.executionsLoading);
|
|
43
|
+
const selectedExecution = useWorkflowsStore((s) => s.selectedExecution);
|
|
44
|
+
const executionDetailLoading = useWorkflowsStore((s) => s.executionDetailLoading);
|
|
45
|
+
const schedulerMetrics = useWorkflowsStore((s) => s.schedulerMetrics);
|
|
46
|
+
const schedulerLoading = useWorkflowsStore((s) => s.schedulerLoading);
|
|
47
|
+
const activeTab = useWorkflowsStore((s) => s.activeTab);
|
|
48
|
+
const error = useWorkflowsStore((s) => s.error);
|
|
49
|
+
|
|
50
|
+
const fetchWorkflows = useWorkflowsStore((s) => s.fetchWorkflows);
|
|
51
|
+
const fetchWorkflowDetail = useWorkflowsStore((s) => s.fetchWorkflowDetail);
|
|
52
|
+
const executeWorkflow = useWorkflowsStore((s) => s.executeWorkflow);
|
|
53
|
+
const fetchExecutions = useWorkflowsStore((s) => s.fetchExecutions);
|
|
54
|
+
const fetchSchedulerMetrics = useWorkflowsStore((s) => s.fetchSchedulerMetrics);
|
|
55
|
+
const deleteWorkflow = useWorkflowsStore((s) => s.deleteWorkflow);
|
|
56
|
+
const enableWorkflow = useWorkflowsStore((s) => s.enableWorkflow);
|
|
57
|
+
const disableWorkflow = useWorkflowsStore((s) => s.disableWorkflow);
|
|
58
|
+
const fetchExecutionDetail = useWorkflowsStore((s) => s.fetchExecutionDetail);
|
|
59
|
+
const clearExecutionDetail = useWorkflowsStore((s) => s.clearExecutionDetail);
|
|
60
|
+
const setActiveTab = useWorkflowsStore((s) => s.setActiveTab);
|
|
61
|
+
const setSelectedWorkflowIndex = useWorkflowsStore((s) => s.setSelectedWorkflowIndex);
|
|
62
|
+
const setSelectedExecutionIndex = useWorkflowsStore((s) => s.setSelectedExecutionIndex);
|
|
63
|
+
|
|
64
|
+
const overlayActive = useUiStore((s) => s.overlayActive);
|
|
65
|
+
|
|
66
|
+
// Track in-flight workflow execution
|
|
67
|
+
const [executing, setExecuting] = useState(false);
|
|
68
|
+
|
|
69
|
+
// Confirmation dialog state for destructive delete action
|
|
70
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
71
|
+
|
|
72
|
+
const handleConfirmDelete = useCallback(() => {
|
|
73
|
+
if (!client) return;
|
|
74
|
+
const wf = workflows[selectedWorkflowIndex];
|
|
75
|
+
if (wf) {
|
|
76
|
+
deleteWorkflow(wf.name, client);
|
|
77
|
+
}
|
|
78
|
+
setConfirmDelete(false);
|
|
79
|
+
}, [client, workflows, selectedWorkflowIndex, deleteWorkflow]);
|
|
80
|
+
|
|
81
|
+
const handleCancelDelete = useCallback(() => {
|
|
82
|
+
setConfirmDelete(false);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// Refresh current view based on active tab
|
|
86
|
+
const refreshCurrentView = (): void => {
|
|
87
|
+
if (!client) return;
|
|
88
|
+
|
|
89
|
+
if (activeTab === "workflows") {
|
|
90
|
+
fetchWorkflows(client);
|
|
91
|
+
} else if (activeTab === "executions") {
|
|
92
|
+
const wf = workflows[selectedWorkflowIndex];
|
|
93
|
+
if (wf) {
|
|
94
|
+
fetchExecutions(wf.name, client);
|
|
95
|
+
}
|
|
96
|
+
} else if (activeTab === "scheduler") {
|
|
97
|
+
fetchSchedulerMetrics(client);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Auto-fetch when tab changes
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
refreshCurrentView();
|
|
104
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
|
+
}, [activeTab, client]);
|
|
106
|
+
|
|
107
|
+
// Resolve the list length for current tab navigation
|
|
108
|
+
const currentListLength = (): number => {
|
|
109
|
+
if (activeTab === "workflows") return workflows.length;
|
|
110
|
+
if (activeTab === "executions") return executions.length;
|
|
111
|
+
return 0;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const currentIndex = (): number => {
|
|
115
|
+
if (activeTab === "workflows") return selectedWorkflowIndex;
|
|
116
|
+
if (activeTab === "executions") return selectedExecutionIndex;
|
|
117
|
+
return 0;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const setCurrentIndex = (index: number): void => {
|
|
121
|
+
if (activeTab === "workflows") {
|
|
122
|
+
setSelectedWorkflowIndex(index);
|
|
123
|
+
} else if (activeTab === "executions") {
|
|
124
|
+
setSelectedExecutionIndex(index);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
useKeyboard(
|
|
129
|
+
overlayActive
|
|
130
|
+
? {}
|
|
131
|
+
: confirmDelete
|
|
132
|
+
? {} // ConfirmDialog handles its own keys when visible
|
|
133
|
+
: {
|
|
134
|
+
...listNavigationBindings({
|
|
135
|
+
getIndex: currentIndex,
|
|
136
|
+
setIndex: setCurrentIndex,
|
|
137
|
+
getLength: currentListLength,
|
|
138
|
+
}),
|
|
139
|
+
...subTabCycleBindings(visibleTabs, activeTab, setActiveTab),
|
|
140
|
+
r: () => refreshCurrentView(),
|
|
141
|
+
e: () => {
|
|
142
|
+
if (activeTab !== "workflows" || !client) return;
|
|
143
|
+
const wf = workflows[selectedWorkflowIndex];
|
|
144
|
+
if (wf && wf.enabled) {
|
|
145
|
+
setExecuting(true);
|
|
146
|
+
executeWorkflow(wf.name, client).finally(() => setExecuting(false));
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
d: () => {
|
|
150
|
+
if (activeTab !== "workflows") return;
|
|
151
|
+
const wf = workflows[selectedWorkflowIndex];
|
|
152
|
+
if (wf) setConfirmDelete(true);
|
|
153
|
+
},
|
|
154
|
+
p: () => {
|
|
155
|
+
if (activeTab !== "workflows" || !client) return;
|
|
156
|
+
const wf = workflows[selectedWorkflowIndex];
|
|
157
|
+
if (wf) {
|
|
158
|
+
if (wf.enabled) disableWorkflow(wf.name, client);
|
|
159
|
+
else enableWorkflow(wf.name, client);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
return: () => {
|
|
163
|
+
if (!client) return;
|
|
164
|
+
|
|
165
|
+
if (activeTab === "workflows") {
|
|
166
|
+
const wf = workflows[selectedWorkflowIndex];
|
|
167
|
+
if (wf) {
|
|
168
|
+
fetchWorkflowDetail(wf.name, client);
|
|
169
|
+
}
|
|
170
|
+
} else if (activeTab === "executions") {
|
|
171
|
+
const ex = executions[selectedExecutionIndex];
|
|
172
|
+
if (ex) {
|
|
173
|
+
// Toggle: if detail is shown for this execution, clear it
|
|
174
|
+
if (selectedExecution?.execution_id === ex.execution_id) {
|
|
175
|
+
clearExecutionDetail();
|
|
176
|
+
} else {
|
|
177
|
+
fetchExecutionDetail(ex.execution_id, client);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
escape: () => {
|
|
183
|
+
// Clear expanded detail views
|
|
184
|
+
if (activeTab === "executions" && selectedExecution) {
|
|
185
|
+
clearExecutionDetail();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<BrickGate brick={["workflows", "scheduler"]}>
|
|
193
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
194
|
+
<Tooltip tooltipKey="workflows-panel" message="Tip: Press ? for keybinding help" />
|
|
195
|
+
{/* Tab bar */}
|
|
196
|
+
<box height={1} width="100%">
|
|
197
|
+
<text>
|
|
198
|
+
{TAB_ORDER.map((tab) => {
|
|
199
|
+
const label = TAB_LABELS[tab];
|
|
200
|
+
return tab === activeTab ? `[${label}]` : ` ${label} `;
|
|
201
|
+
}).join(" ")}
|
|
202
|
+
</text>
|
|
203
|
+
</box>
|
|
204
|
+
|
|
205
|
+
{/* Error display */}
|
|
206
|
+
{error && (
|
|
207
|
+
<box height={1} width="100%">
|
|
208
|
+
<text>{`Error: ${error}`}</text>
|
|
209
|
+
</box>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Execution in-flight indicator */}
|
|
213
|
+
{executing && (
|
|
214
|
+
<box height={1} width="100%">
|
|
215
|
+
<LoadingIndicator message="Executing workflow..." centered={false} />
|
|
216
|
+
</box>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* Detail content */}
|
|
220
|
+
<box flexGrow={1} borderStyle="single">
|
|
221
|
+
{activeTab === "workflows" && (
|
|
222
|
+
<WorkflowList
|
|
223
|
+
workflows={workflows}
|
|
224
|
+
selectedIndex={selectedWorkflowIndex}
|
|
225
|
+
loading={workflowsLoading}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
{activeTab === "executions" && (
|
|
229
|
+
<ExecutionList
|
|
230
|
+
executions={executions}
|
|
231
|
+
selectedIndex={selectedExecutionIndex}
|
|
232
|
+
loading={executionsLoading}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
{activeTab === "scheduler" && (
|
|
236
|
+
<SchedulerView
|
|
237
|
+
metrics={schedulerMetrics}
|
|
238
|
+
loading={schedulerLoading}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
</box>
|
|
242
|
+
|
|
243
|
+
{/* Execution detail overlay when loaded */}
|
|
244
|
+
{activeTab === "executions" && executionDetailLoading && (
|
|
245
|
+
<box height={1} width="100%">
|
|
246
|
+
<LoadingIndicator message="Loading execution detail..." centered={false} />
|
|
247
|
+
</box>
|
|
248
|
+
)}
|
|
249
|
+
{activeTab === "executions" && selectedExecution && !executionDetailLoading && (
|
|
250
|
+
<box height={Math.min((selectedExecution.steps?.length ?? 0) + 3, 12)} width="100%" borderStyle="single" flexDirection="column">
|
|
251
|
+
<text>
|
|
252
|
+
{`Execution: ${selectedExecution.execution_id} | ${selectedExecution.status} | ${selectedExecution.actions_completed}/${selectedExecution.actions_total} actions`}
|
|
253
|
+
</text>
|
|
254
|
+
{(selectedExecution.steps ?? []).length > 0 ? (
|
|
255
|
+
<scrollbox flexGrow={1} width="100%">
|
|
256
|
+
{(selectedExecution.steps ?? []).map((step, i) => (
|
|
257
|
+
<box key={i} height={1} width="100%">
|
|
258
|
+
<text>
|
|
259
|
+
{` ${String(step.step_index).padEnd(3)} ${(step.action_name ?? "").padEnd(20)} ${step.status.padEnd(10)} ${step.error_message ? `ERR: ${step.error_message}` : ""}`}
|
|
260
|
+
</text>
|
|
261
|
+
</box>
|
|
262
|
+
))}
|
|
263
|
+
</scrollbox>
|
|
264
|
+
) : (
|
|
265
|
+
<text style={textStyle({ dim: true })}> No steps recorded</text>
|
|
266
|
+
)}
|
|
267
|
+
</box>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{/* Workflow detail overlay when loaded */}
|
|
271
|
+
{selectedWorkflow && activeTab === "workflows" && !detailLoading && (
|
|
272
|
+
<box height={3} width="100%">
|
|
273
|
+
<text>
|
|
274
|
+
{`Detail: ${selectedWorkflow.name} | v${selectedWorkflow.version} | ${selectedWorkflow.enabled ? "enabled" : "disabled"} | ${selectedWorkflow.triggers.length} triggers | ${selectedWorkflow.actions.length} actions`}
|
|
275
|
+
</text>
|
|
276
|
+
</box>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* Help bar */}
|
|
280
|
+
<box height={1} width="100%">
|
|
281
|
+
<text>{HELP_TEXT[activeTab] ?? ""}</text>
|
|
282
|
+
</box>
|
|
283
|
+
|
|
284
|
+
{/* Delete confirmation dialog */}
|
|
285
|
+
<ConfirmDialog
|
|
286
|
+
visible={confirmDelete}
|
|
287
|
+
title="Delete Workflow"
|
|
288
|
+
message={`Permanently delete "${workflows[selectedWorkflowIndex]?.name ?? ""}"? This cannot be undone.`}
|
|
289
|
+
onConfirm={handleConfirmDelete}
|
|
290
|
+
onCancel={handleCancelDelete}
|
|
291
|
+
/>
|
|
292
|
+
</box>
|
|
293
|
+
</BrickGate>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brick detail view: shows individual brick info from GET /api/v2/bricks/{name}.
|
|
3
|
+
*
|
|
4
|
+
* Displays: name, state, protocol, error, dependency graph, config (spec),
|
|
5
|
+
* real FSM transition history, and available actions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import type { BrickDetailResponse } from "../../stores/zones-store.js";
|
|
10
|
+
import { stateIndicator, allowedActionsForState } from "../../shared/brick-states.js";
|
|
11
|
+
|
|
12
|
+
interface BrickDetailProps {
|
|
13
|
+
readonly brick: BrickDetailResponse | null;
|
|
14
|
+
readonly loading: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatEpoch(epoch: number | null): string {
|
|
18
|
+
if (epoch === null) return "n/a";
|
|
19
|
+
try {
|
|
20
|
+
return new Date(epoch * 1000).toLocaleString();
|
|
21
|
+
} catch {
|
|
22
|
+
return String(epoch);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ACTION_KEYS: Readonly<Record<string, string>> = {
|
|
27
|
+
mount: "M (shift)",
|
|
28
|
+
unmount: "U",
|
|
29
|
+
unregister: "D",
|
|
30
|
+
remount: "m",
|
|
31
|
+
reset: "x",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function BrickDetail({ brick, loading }: BrickDetailProps): React.ReactNode {
|
|
35
|
+
if (loading) {
|
|
36
|
+
return (
|
|
37
|
+
<box height="100%" width="100%" justifyContent="center" alignItems="center">
|
|
38
|
+
<text>Loading brick detail...</text>
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!brick) {
|
|
44
|
+
return (
|
|
45
|
+
<box height="100%" width="100%" justifyContent="center" alignItems="center">
|
|
46
|
+
<text>Select a brick to view details</text>
|
|
47
|
+
</box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const allowed = allowedActionsForState(brick.state);
|
|
52
|
+
const actionHints = Array.from(allowed)
|
|
53
|
+
.map((action) => {
|
|
54
|
+
const key = ACTION_KEYS[action] ?? action;
|
|
55
|
+
return `${key}:${action}`;
|
|
56
|
+
})
|
|
57
|
+
.join(" ");
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<scrollbox height="100%" width="100%">
|
|
61
|
+
{/* Identity */}
|
|
62
|
+
<box height={1} width="100%">
|
|
63
|
+
<text>{`Name: ${brick.name}`}</text>
|
|
64
|
+
</box>
|
|
65
|
+
<box height={1} width="100%">
|
|
66
|
+
<text>{`State: ${stateIndicator(brick.state)} ${brick.state}`}</text>
|
|
67
|
+
</box>
|
|
68
|
+
<box height={1} width="100%">
|
|
69
|
+
<text>{`Protocol: ${brick.protocol_name}`}</text>
|
|
70
|
+
</box>
|
|
71
|
+
<box height={1} width="100%">
|
|
72
|
+
<text>{`Error: ${brick.error ?? "none"}`}</text>
|
|
73
|
+
</box>
|
|
74
|
+
|
|
75
|
+
{/* Config (spec data) */}
|
|
76
|
+
<box height={1} width="100%" marginTop={1}>
|
|
77
|
+
<text>--- Configuration ---</text>
|
|
78
|
+
</box>
|
|
79
|
+
<box height={1} width="100%">
|
|
80
|
+
<text>{`Enabled: ${brick.enabled ? "yes" : "no"}`}</text>
|
|
81
|
+
</box>
|
|
82
|
+
<box height={1} width="100%">
|
|
83
|
+
<text>{`Retry count: ${brick.retry_count}`}</text>
|
|
84
|
+
</box>
|
|
85
|
+
|
|
86
|
+
{/* Dependency graph */}
|
|
87
|
+
<box height={1} width="100%" marginTop={1}>
|
|
88
|
+
<text>--- Dependencies ---</text>
|
|
89
|
+
</box>
|
|
90
|
+
<box height={1} width="100%">
|
|
91
|
+
<text>{`Depends on: ${brick.depends_on.length > 0 ? brick.depends_on.join(", ") : "(none)"}`}</text>
|
|
92
|
+
</box>
|
|
93
|
+
<box height={1} width="100%">
|
|
94
|
+
<text>{`Depended by: ${brick.depended_by.length > 0 ? brick.depended_by.join(", ") : "(none)"}`}</text>
|
|
95
|
+
</box>
|
|
96
|
+
|
|
97
|
+
{/* Timestamps */}
|
|
98
|
+
<box height={1} width="100%" marginTop={1}>
|
|
99
|
+
<text>--- Timestamps ---</text>
|
|
100
|
+
</box>
|
|
101
|
+
<box height={1} width="100%">
|
|
102
|
+
<text>{`Started at: ${formatEpoch(brick.started_at)}`}</text>
|
|
103
|
+
</box>
|
|
104
|
+
<box height={1} width="100%">
|
|
105
|
+
<text>{`Stopped at: ${formatEpoch(brick.stopped_at)}`}</text>
|
|
106
|
+
</box>
|
|
107
|
+
<box height={1} width="100%">
|
|
108
|
+
<text>{`Unmounted at: ${formatEpoch(brick.unmounted_at)}`}</text>
|
|
109
|
+
</box>
|
|
110
|
+
|
|
111
|
+
{/* State history (real FSM transitions) */}
|
|
112
|
+
<box height={1} width="100%" marginTop={1}>
|
|
113
|
+
<text>--- State History ---</text>
|
|
114
|
+
</box>
|
|
115
|
+
{brick.transitions.length === 0 ? (
|
|
116
|
+
<box height={1} width="100%">
|
|
117
|
+
<text> No transitions recorded</text>
|
|
118
|
+
</box>
|
|
119
|
+
) : (
|
|
120
|
+
brick.transitions.map((t, i) => (
|
|
121
|
+
<box key={i} height={1} width="100%">
|
|
122
|
+
<text>{` ${formatEpoch(t.timestamp)} ${t.from_state} → ${t.to_state} (${t.event})`}</text>
|
|
123
|
+
</box>
|
|
124
|
+
))
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{/* Available actions */}
|
|
128
|
+
<box height={1} width="100%" marginTop={1}>
|
|
129
|
+
<text>--- Available Actions ---</text>
|
|
130
|
+
</box>
|
|
131
|
+
<box height={1} width="100%">
|
|
132
|
+
<text>{actionHints || "(none — brick is in a transient state)"}</text>
|
|
133
|
+
</box>
|
|
134
|
+
</scrollbox>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brick list sidebar: state indicator + name + protocol for each brick.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { BrickStatusResponse } from "../../stores/zones-store.js";
|
|
7
|
+
import { stateIndicator, stateColor } from "../../shared/brick-states.js";
|
|
8
|
+
import { EmptyState } from "../../shared/components/empty-state.js";
|
|
9
|
+
import { textStyle } from "../../shared/text-style.js";
|
|
10
|
+
|
|
11
|
+
interface BrickListProps {
|
|
12
|
+
readonly bricks: readonly BrickStatusResponse[];
|
|
13
|
+
readonly selectedIndex: number;
|
|
14
|
+
readonly loading: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function BrickList({
|
|
18
|
+
bricks,
|
|
19
|
+
selectedIndex,
|
|
20
|
+
loading,
|
|
21
|
+
}: BrickListProps): React.ReactNode {
|
|
22
|
+
if (loading) {
|
|
23
|
+
return (
|
|
24
|
+
<box height="100%" width="100%" justifyContent="center" alignItems="center">
|
|
25
|
+
<text>Loading bricks...</text>
|
|
26
|
+
</box>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (bricks.length === 0) {
|
|
31
|
+
return (
|
|
32
|
+
<EmptyState
|
|
33
|
+
message="No bricks registered."
|
|
34
|
+
hint="Run 'nexus brick mount <name>' to add one."
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<scrollbox flexGrow={1} width="100%">
|
|
41
|
+
{bricks.map((brick, i) => {
|
|
42
|
+
const isSelected = i === selectedIndex;
|
|
43
|
+
const prefix = isSelected ? "> " : " ";
|
|
44
|
+
const indicator = stateIndicator(brick.state);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<box key={brick.name} height={1} width="100%">
|
|
48
|
+
<text>{prefix}</text>
|
|
49
|
+
<text style={textStyle({ fg: stateColor(brick.state) })}>{indicator}</text>
|
|
50
|
+
<text>{` ${brick.name} (${brick.protocol_name})`}</text>
|
|
51
|
+
</box>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</scrollbox>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { textStyle } from "../../shared/text-style.js";
|
|
3
|
+
import { formatSize } from "../../shared/utils/format-size.js";
|
|
4
|
+
import { statusColor } from "../../shared/theme.js";
|
|
5
|
+
|
|
6
|
+
interface CacheStats {
|
|
7
|
+
readonly total_entries?: number;
|
|
8
|
+
readonly total_size_bytes?: number;
|
|
9
|
+
readonly hit_rate?: number;
|
|
10
|
+
readonly miss_rate?: number;
|
|
11
|
+
readonly eviction_count?: number;
|
|
12
|
+
readonly layers?: ReadonlyArray<{
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly entries: number;
|
|
15
|
+
readonly size_bytes: number;
|
|
16
|
+
readonly hit_rate: number;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface HotFile {
|
|
21
|
+
readonly path?: string;
|
|
22
|
+
readonly access_count?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CacheTabProps {
|
|
26
|
+
readonly stats: unknown | null;
|
|
27
|
+
readonly hotFiles: readonly unknown[];
|
|
28
|
+
readonly loading: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hitRateColor(rate: number): string | undefined {
|
|
32
|
+
if (rate > 0.8) return statusColor.healthy;
|
|
33
|
+
if (rate > 0.5) return statusColor.warning;
|
|
34
|
+
return statusColor.error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function CacheTab({ stats, hotFiles, loading }: CacheTabProps): React.ReactNode {
|
|
38
|
+
if (loading) return <text>Loading cache stats...</text>;
|
|
39
|
+
if (!stats) return <text>No cache data available.</text>;
|
|
40
|
+
|
|
41
|
+
const s = stats as CacheStats;
|
|
42
|
+
const files = hotFiles as readonly HotFile[];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
46
|
+
<box height={1} width="100%"><text>--- Cache Statistics ---</text></box>
|
|
47
|
+
|
|
48
|
+
{/* Summary stats */}
|
|
49
|
+
{s.total_entries != null && (
|
|
50
|
+
<box height={1} width="100%">
|
|
51
|
+
<text>{` Total entries: ${s.total_entries.toLocaleString()}`}</text>
|
|
52
|
+
</box>
|
|
53
|
+
)}
|
|
54
|
+
{s.total_size_bytes != null && (
|
|
55
|
+
<box height={1} width="100%">
|
|
56
|
+
<text>{` Total size: ${formatSize(s.total_size_bytes)}`}</text>
|
|
57
|
+
</box>
|
|
58
|
+
)}
|
|
59
|
+
{s.hit_rate != null && (
|
|
60
|
+
<box height={1} width="100%">
|
|
61
|
+
<text>
|
|
62
|
+
{" Hit rate: "}
|
|
63
|
+
<span style={textStyle({ fg: hitRateColor(s.hit_rate) })}>{`${(s.hit_rate * 100).toFixed(1)}%`}</span>
|
|
64
|
+
</text>
|
|
65
|
+
</box>
|
|
66
|
+
)}
|
|
67
|
+
{s.miss_rate != null && (
|
|
68
|
+
<box height={1} width="100%">
|
|
69
|
+
<text>{` Miss rate: ${(s.miss_rate * 100).toFixed(1)}%`}</text>
|
|
70
|
+
</box>
|
|
71
|
+
)}
|
|
72
|
+
{s.eviction_count != null && (
|
|
73
|
+
<box height={1} width="100%">
|
|
74
|
+
<text>{` Evictions: ${s.eviction_count.toLocaleString()}`}</text>
|
|
75
|
+
</box>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{/* Layer table */}
|
|
79
|
+
{s.layers && s.layers.length > 0 && (
|
|
80
|
+
<>
|
|
81
|
+
<box height={1} width="100%"><text>{""}</text></box>
|
|
82
|
+
<box height={1} width="100%"><text>--- Cache Layers ---</text></box>
|
|
83
|
+
<box height={1} width="100%">
|
|
84
|
+
<text>{" NAME ENTRIES SIZE HIT RATE"}</text>
|
|
85
|
+
</box>
|
|
86
|
+
<box height={1} width="100%">
|
|
87
|
+
<text>{" ------------------- ---------- ----------- --------"}</text>
|
|
88
|
+
</box>
|
|
89
|
+
{s.layers.map((layer) => (
|
|
90
|
+
<box key={layer.name} height={1} width="100%">
|
|
91
|
+
<text>
|
|
92
|
+
{` ${layer.name.padEnd(19)} ${String(layer.entries).padEnd(10)} ${formatSize(layer.size_bytes).padEnd(11)} `}
|
|
93
|
+
<span style={textStyle({ fg: hitRateColor(layer.hit_rate) })}>{`${(layer.hit_rate * 100).toFixed(1)}%`}</span>
|
|
94
|
+
</text>
|
|
95
|
+
</box>
|
|
96
|
+
))}
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{/* Hot files */}
|
|
101
|
+
{files.length > 0 && (
|
|
102
|
+
<>
|
|
103
|
+
<box height={1} width="100%"><text>{""}</text></box>
|
|
104
|
+
<box height={1} width="100%"><text>--- Hot Files ---</text></box>
|
|
105
|
+
{files.slice(0, 10).map((file, i) => {
|
|
106
|
+
const path = file.path ?? "unknown";
|
|
107
|
+
const count = file.access_count ?? 0;
|
|
108
|
+
return (
|
|
109
|
+
<box key={`hf-${i}`} height={1} width="100%">
|
|
110
|
+
<text>{` ${path} (${count} hits)`}</text>
|
|
111
|
+
</box>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
</box>
|
|
117
|
+
);
|
|
118
|
+
}
|