@outputai/cli 0.4.1-next.d43aa3d.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/docker/docker-compose-dev.yml +1 -4
- package/dist/generated/framework_version.json +1 -1
- package/dist/views/dev/chrome/footer.d.ts +5 -4
- package/dist/views/dev/chrome/footer.js +12 -2
- package/dist/views/dev/chrome/header.d.ts +2 -1
- package/dist/views/dev/chrome/header.js +18 -47
- package/dist/views/dev/chrome/header.spec.js +6 -46
- package/dist/views/dev/chrome/layout_heights.spec.d.ts +1 -0
- package/dist/views/dev/chrome/layout_heights.spec.js +19 -0
- package/dist/views/dev/chrome/search_bar.d.ts +1 -1
- package/dist/views/dev/chrome/search_bar.js +10 -11
- package/dist/views/dev/chrome/tab_bar.d.ts +8 -1
- package/dist/views/dev/chrome/tab_bar.js +16 -2
- package/dist/views/dev/chrome/toasts.d.ts +1 -0
- package/dist/views/dev/chrome/toasts.js +8 -4
- package/dist/views/dev/components/content_title.d.ts +6 -0
- package/dist/views/dev/components/content_title.js +7 -0
- package/dist/views/dev/components/docker_service_status.d.ts +12 -0
- package/dist/views/dev/components/docker_service_status.js +19 -0
- package/dist/views/dev/components/inline_snippet.d.ts +4 -0
- package/dist/views/dev/components/inline_snippet.js +3 -0
- package/dist/views/dev/components/master_detail_panel.d.ts +9 -8
- package/dist/views/dev/components/master_detail_panel.js +8 -5
- package/dist/views/dev/components/run_info_sidebar.d.ts +7 -0
- package/dist/views/dev/components/run_info_sidebar.js +19 -0
- package/dist/views/dev/components/workflow_status.d.ts +12 -0
- package/dist/views/dev/components/workflow_status.js +19 -0
- package/dist/views/dev/dev_app.js +107 -31
- package/dist/views/dev/hooks/use_run_detail.js +6 -9
- package/dist/views/dev/hooks/use_run_detail.spec.js +7 -0
- package/dist/views/dev/modals/expanded_json_modal.js +5 -6
- package/dist/views/dev/modals/modal_frame.d.ts +13 -0
- package/dist/views/dev/modals/modal_frame.js +13 -0
- package/dist/views/dev/modals/run_modal.js +23 -13
- package/dist/views/dev/{panels/run_detail_view.d.ts → modals/steps_modal.d.ts} +2 -1
- package/dist/views/dev/modals/steps_modal.js +102 -0
- package/dist/views/dev/panels/help_panel.d.ts +14 -0
- package/dist/views/dev/panels/help_panel.js +19 -21
- package/dist/views/dev/panels/runs_panel.d.ts +6 -2
- package/dist/views/dev/panels/runs_panel.js +82 -83
- package/dist/views/dev/panels/runs_panel.spec.js +1 -28
- package/dist/views/dev/panels/services_panel.d.ts +6 -0
- package/dist/views/dev/panels/services_panel.js +53 -62
- package/dist/views/dev/panels/workflows_panel.d.ts +6 -0
- package/dist/views/dev/panels/workflows_panel.js +21 -29
- package/dist/views/dev/panels/workflows_panel.spec.d.ts +1 -0
- package/dist/views/dev/panels/workflows_panel.spec.js +39 -0
- package/dist/views/dev/state/ui_state.d.ts +7 -3
- package/dist/views/dev/state/ui_state.js +23 -6
- package/dist/views/dev/utils/constants.d.ts +2 -2
- package/dist/views/dev/utils/constants.js +2 -2
- package/dist/views/dev/utils/json_editor.js +3 -3
- package/dist/views/dev/utils/json_render.d.ts +2 -0
- package/dist/views/dev/utils/json_render.js +48 -6
- package/dist/views/dev/utils/json_render.spec.js +9 -1
- package/dist/views/dev/utils/panel_helpers.d.ts +15 -0
- package/dist/views/dev/utils/panel_helpers.js +30 -0
- package/dist/views/dev/utils/panel_helpers.spec.js +46 -1
- package/package.json +4 -4
- package/dist/components/status_icon.d.ts +0 -11
- package/dist/components/status_icon.js +0 -25
- package/dist/views/dev/chrome/divider.d.ts +0 -8
- package/dist/views/dev/chrome/divider.js +0 -16
- package/dist/views/dev/panels/run_detail_view.js +0 -112
|
@@ -77,7 +77,7 @@ services:
|
|
|
77
77
|
condition: service_healthy
|
|
78
78
|
worker:
|
|
79
79
|
condition: service_healthy
|
|
80
|
-
image: outputai/api:${OUTPUT_API_VERSION:-0.
|
|
80
|
+
image: outputai/api:${OUTPUT_API_VERSION:-0.5.0}
|
|
81
81
|
init: true
|
|
82
82
|
networks:
|
|
83
83
|
- main
|
|
@@ -88,9 +88,6 @@ services:
|
|
|
88
88
|
- NODE_ENV=development
|
|
89
89
|
- OUTPUT_API_PORT=3001
|
|
90
90
|
- OUTPUT_CATALOG_ID=${OUTPUT_CATALOG_ID:-main}
|
|
91
|
-
- OUTPUT_AWS_REGION=${AWS_REGION:-us-west-1}
|
|
92
|
-
- OUTPUT_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
|
93
|
-
- OUTPUT_AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
|
|
94
91
|
- TEMPORAL_ADDRESS=temporal:7233
|
|
95
92
|
ports:
|
|
96
93
|
- '${OUTPUT_API_HOST_PORT:-3001}:3001'
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
export declare const getHeight: () => number;
|
|
2
3
|
export interface CommandHint {
|
|
3
4
|
key: string;
|
|
4
5
|
label: string;
|
|
5
6
|
}
|
|
6
|
-
export
|
|
7
|
-
hints
|
|
7
|
+
export interface FooterState {
|
|
8
|
+
hints?: CommandHint[];
|
|
8
9
|
itemCount?: number;
|
|
9
10
|
itemLabel?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare const
|
|
11
|
+
}
|
|
12
|
+
export declare const Footer: React.FC<FooterState>;
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import packageJson from '../../../../package.json' with { type: 'json' };
|
|
5
|
+
const GLOBAL_HINT_ROWS = 1;
|
|
6
|
+
const LOCAL_HINT_ROWS = 1;
|
|
7
|
+
export const getHeight = () => GLOBAL_HINT_ROWS + LOCAL_HINT_ROWS;
|
|
8
|
+
const GLOBAL_HINTS = [
|
|
6
9
|
{ key: 'tab', label: 'next tab' },
|
|
10
|
+
{ key: 'shift-tab', label: 'prev tab' },
|
|
11
|
+
{ key: '1-4', label: 'tabs' },
|
|
7
12
|
{ key: '/', label: 'search' },
|
|
8
13
|
{ key: '?', label: 'help' },
|
|
9
14
|
{ key: 'ctrl+c', label: 'quit' }
|
|
10
15
|
];
|
|
16
|
+
const VERSION = packageJson.version;
|
|
17
|
+
const HintRow = ({ hints }) => (_jsx(Box, { flexDirection: "row", children: hints.length === 0 ? (_jsx(Text, { children: " " })) : hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }));
|
|
18
|
+
export const Footer = ({ hints = [], itemCount, itemLabel }) => {
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints: GLOBAL_HINTS }), typeof itemCount === 'number' && itemLabel && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [itemCount, " ", itemLabel] }) }))] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints: hints }), _jsxs(Text, { color: "blackBright", children: ["v", VERSION] })] })] }));
|
|
20
|
+
};
|
|
@@ -5,7 +5,8 @@ export interface WorkflowSummary {
|
|
|
5
5
|
failed: number;
|
|
6
6
|
total: number;
|
|
7
7
|
}
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const getHeight: (terminalRows: number) => number;
|
|
9
|
+
export declare const useHeaderRows: () => number;
|
|
9
10
|
export type ServiceBadge = 'healthy' | 'starting' | 'failed';
|
|
10
11
|
export interface HeaderCounters {
|
|
11
12
|
running: number;
|
|
@@ -3,46 +3,20 @@ import { Box, Text, useStdout } from 'ink';
|
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { LOGO_GRADIENT, PURPLE_100 } from '#views/dev/chrome/palette.js';
|
|
5
5
|
const LOGO_PIXELS = [
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
10
|
-
' ██████ ██████ ██ ██ ██████ ██ '
|
|
6
|
+
'▟▀▀▙▐▌ ▐▌▀▜▛▀▐▛▀▙▐▌ ▐▌▀▜▛▀',
|
|
7
|
+
'█ █▐▌ ▐▌ ▐▌ ▐▛▀▘▐▌ ▐▌ ▐▌ ',
|
|
8
|
+
'▝▀▀▘ ▀▀▀ ▝▘ ▝▘ ▀▀▀ ▝▘ '
|
|
11
9
|
];
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const padded = rows.map(r => r.padEnd(evenCol, ' '));
|
|
22
|
-
const fullPadded = padded.length % 2 === 1 ?
|
|
23
|
-
[...padded, ' '.repeat(evenCol)] :
|
|
24
|
-
padded;
|
|
25
|
-
const rowPairs = fullPadded.reduce((acc, row, i) => {
|
|
26
|
-
if (i % 2 === 0) {
|
|
27
|
-
acc.push([row, fullPadded[i + 1]]);
|
|
28
|
-
}
|
|
29
|
-
return acc;
|
|
30
|
-
}, []);
|
|
31
|
-
const colCount = Math.floor(evenCol / 2);
|
|
32
|
-
return rowPairs.map(([top, bot]) => {
|
|
33
|
-
const chars = Array.from({ length: colCount }, (_, k) => {
|
|
34
|
-
const j = k * 2;
|
|
35
|
-
const tl = top[j] === '█' ? 8 : 0;
|
|
36
|
-
const tr = top[j + 1] === '█' ? 4 : 0;
|
|
37
|
-
const bl = bot[j] === '█' ? 2 : 0;
|
|
38
|
-
const br = bot[j + 1] === '█' ? 1 : 0;
|
|
39
|
-
return QUADRANT_CHARS[tl + tr + bl + br];
|
|
40
|
-
});
|
|
41
|
-
return chars.join('');
|
|
42
|
-
});
|
|
10
|
+
const HEADER_MARGIN = 1;
|
|
11
|
+
const COMPACT_HEIGHT_THRESHOLD = 50;
|
|
12
|
+
const COMPACT_HEADER_ROWS = 1;
|
|
13
|
+
const FULL_HEADER_ROWS = 3;
|
|
14
|
+
const getLogoHeight = (terminalRows) => terminalRows < COMPACT_HEIGHT_THRESHOLD ? COMPACT_HEADER_ROWS : FULL_HEADER_ROWS;
|
|
15
|
+
export const getHeight = (terminalRows) => getLogoHeight(terminalRows) + HEADER_MARGIN;
|
|
16
|
+
export const useHeaderRows = () => {
|
|
17
|
+
const { stdout } = useStdout();
|
|
18
|
+
return getLogoHeight(stdout?.rows ?? 60);
|
|
43
19
|
};
|
|
44
|
-
const LOGO_COMPRESSED = compressPixels(LOGO_PIXELS);
|
|
45
|
-
const FULL_WIDTH_THRESHOLD = 60;
|
|
46
20
|
const ServicesBadge = ({ badge, failingCount }) => {
|
|
47
21
|
if (badge === 'failed') {
|
|
48
22
|
return (_jsxs(Text, { color: "red", bold: true, children: ["\u26A0 ", failingCount, " service", failingCount === 1 ? '' : 's', " down"] }));
|
|
@@ -53,12 +27,9 @@ const ServicesBadge = ({ badge, failingCount }) => {
|
|
|
53
27
|
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", children: "\u25CF " }), _jsx(Text, { children: "services" })] }));
|
|
54
28
|
};
|
|
55
29
|
const Counters = ({ counters }) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 3, children: _jsx(ServicesBadge, { badge: counters.serviceBadge, failingCount: counters.failingServices }) }), counters.running > 0 && (_jsxs(Box, { marginRight: 3, children: [_jsx(Text, { color: "blue", children: "\u25CF " }), _jsxs(Text, { bold: true, children: [counters.running, " "] }), _jsx(Text, { children: "running" })] })), counters.failed > 0 && (_jsxs(Box, { marginRight: 3, children: [_jsx(Text, { color: "red", children: "\u2717 " }), _jsxs(Text, { color: "red", bold: true, children: [counters.failed, " "] }), _jsx(Text, { color: "red", children: "failed" })] })), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [counters.totalWorkflows, " workflows \u00B7 ", counters.totalRuns, " runs"] }) })] }));
|
|
56
|
-
const Logo = ({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
return (_jsx(Box, { flexDirection: "column", children: LOGO_COMPRESSED.map((line, i) => (_jsx(Text, { color: LOGO_GRADIENT[i] ?? PURPLE_100, bold: true, children: line }, i))) }));
|
|
61
|
-
};
|
|
30
|
+
const Logo = ({ compact }) => (_jsx(Box, { flexDirection: "column", children: compact ?
|
|
31
|
+
_jsx(Text, { color: PURPLE_100, bold: true, children: "OUTPUT" }) :
|
|
32
|
+
LOGO_PIXELS.map((line, i) => (_jsx(Text, { color: LOGO_GRADIENT[i] ?? PURPLE_100, bold: true, children: line }, i))) }));
|
|
62
33
|
export const buildSummaryCounters = (summary, totalWorkflows, serviceBadge = 'starting', failingServices = 0) => ({
|
|
63
34
|
running: summary?.running ?? 0,
|
|
64
35
|
failed: summary?.failed ?? 0,
|
|
@@ -68,7 +39,7 @@ export const buildSummaryCounters = (summary, totalWorkflows, serviceBadge = 'st
|
|
|
68
39
|
failingServices
|
|
69
40
|
});
|
|
70
41
|
export const Header = ({ counters }) => {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "
|
|
42
|
+
const headerRows = useHeaderRows();
|
|
43
|
+
const compact = headerRows === COMPACT_HEADER_ROWS;
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: HEADER_MARGIN, children: [_jsx(Logo, { compact: compact }), _jsx(Box, { flexDirection: "column", children: _jsx(Counters, { counters: counters }) })] }));
|
|
74
45
|
};
|
|
@@ -1,50 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
describe('
|
|
4
|
-
it('
|
|
5
|
-
expect(
|
|
2
|
+
import { getHeight as getHeaderHeight } from './header.js';
|
|
3
|
+
describe('getHeaderHeight', () => {
|
|
4
|
+
it('includes margin with compact height for terminals under 50 rows', () => {
|
|
5
|
+
expect(getHeaderHeight(49)).toBe(2);
|
|
6
6
|
});
|
|
7
|
-
it('
|
|
8
|
-
expect(
|
|
9
|
-
});
|
|
10
|
-
it('maps each quadrant character correctly', () => {
|
|
11
|
-
// Each row pair decodes to: top-left, top-right, bottom-left, bottom-right
|
|
12
|
-
const cases = [
|
|
13
|
-
[[' ', ' '], ' '], // 0000
|
|
14
|
-
[[' ', ' █'], '▗'], // 0001 bottom-right
|
|
15
|
-
[[' ', '█ '], '▖'], // 0010 bottom-left
|
|
16
|
-
[[' ', '██'], '▄'], // 0011 bottom half
|
|
17
|
-
[[' █', ' '], '▝'], // 0100 top-right
|
|
18
|
-
[[' █', ' █'], '▐'], // 0101 right half
|
|
19
|
-
[[' █', '█ '], '▞'], // 0110 anti-diagonal
|
|
20
|
-
[[' █', '██'], '▟'], // 0111
|
|
21
|
-
[['█ ', ' '], '▘'], // 1000 top-left
|
|
22
|
-
[['█ ', ' █'], '▚'], // 1001 diagonal
|
|
23
|
-
[['█ ', '█ '], '▌'], // 1010 left half
|
|
24
|
-
[['█ ', '██'], '▙'], // 1011
|
|
25
|
-
[['██', ' '], '▀'], // 1100 top half
|
|
26
|
-
[['██', ' █'], '▜'], // 1101
|
|
27
|
-
[['██', '█ '], '▛'], // 1110
|
|
28
|
-
[['██', '██'], '█'] // 1111 full
|
|
29
|
-
];
|
|
30
|
-
for (const [input, expected] of cases) {
|
|
31
|
-
expect(compressPixels(input)).toEqual([expected]);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
it('pads odd-length rows with an empty bottom row', () => {
|
|
35
|
-
// Single row of 4 cols compresses to 1 row of 2 cols
|
|
36
|
-
expect(compressPixels(['████'])).toEqual(['▀▀']);
|
|
37
|
-
});
|
|
38
|
-
it('pads odd-width columns with a trailing space', () => {
|
|
39
|
-
// 3 cols compresses to 2 cols (even-padded)
|
|
40
|
-
expect(compressPixels(['███', '███'])).toEqual(['█▌']);
|
|
41
|
-
});
|
|
42
|
-
it('compresses 5-row input to 3 rows', () => {
|
|
43
|
-
const five = ['██', '██', '██', '██', '██'];
|
|
44
|
-
const result = compressPixels(five);
|
|
45
|
-
expect(result.length).toBe(3);
|
|
46
|
-
expect(result[0]).toBe('█');
|
|
47
|
-
expect(result[1]).toBe('█');
|
|
48
|
-
expect(result[2]).toBe('▀');
|
|
7
|
+
it('includes margin with full height for terminals with at least 50 rows', () => {
|
|
8
|
+
expect(getHeaderHeight(50)).toBe(4);
|
|
49
9
|
});
|
|
50
10
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getHeight as getFooterHeight } from './footer.js';
|
|
3
|
+
import { getHeight as getTabBarHeight } from './tab_bar.js';
|
|
4
|
+
import { getHeight as getContentTitleHeight } from '../components/content_title.js';
|
|
5
|
+
import { getHeight as getModalFrameHeight } from '../modals/modal_frame.js';
|
|
6
|
+
describe('static layout heights', () => {
|
|
7
|
+
it('reports the tab bar height including its border row', () => {
|
|
8
|
+
expect(getTabBarHeight()).toBe(2);
|
|
9
|
+
});
|
|
10
|
+
it('reports the two-line footer height', () => {
|
|
11
|
+
expect(getFooterHeight()).toBe(2);
|
|
12
|
+
});
|
|
13
|
+
it('reports content title height including bottom margin', () => {
|
|
14
|
+
expect(getContentTitleHeight()).toBe(2);
|
|
15
|
+
});
|
|
16
|
+
it('reports modal frame chrome height', () => {
|
|
17
|
+
expect(getModalFrameHeight()).toBe(6);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect } from 'react';
|
|
3
2
|
import { Box, Text, useInput } from 'ink';
|
|
4
3
|
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
5
|
-
|
|
4
|
+
const SEARCH_CONTENT_ROWS = 1;
|
|
5
|
+
const SEARCH_BORDER_ROWS = 2;
|
|
6
|
+
export const useHeight = () => {
|
|
6
7
|
const ui = useUiState();
|
|
8
|
+
return ui.search.open || Boolean(ui.search.query) ? SEARCH_CONTENT_ROWS + SEARCH_BORDER_ROWS : 0;
|
|
9
|
+
};
|
|
10
|
+
export const SearchBar = ({ active }) => {
|
|
11
|
+
const ui = useUiState();
|
|
12
|
+
const visible = useHeight() > 0;
|
|
7
13
|
useInput((input, key) => {
|
|
8
14
|
if (key.escape) {
|
|
9
15
|
ui.clearSearch();
|
|
10
16
|
return;
|
|
11
17
|
}
|
|
12
18
|
if (key.return) {
|
|
13
|
-
onSubmit?.(ui.search.query);
|
|
14
19
|
ui.closeSearch();
|
|
15
20
|
return;
|
|
16
21
|
}
|
|
@@ -22,14 +27,8 @@ export const SearchBar = ({ active, onSubmit }) => {
|
|
|
22
27
|
ui.setSearchQuery(ui.search.query + input);
|
|
23
28
|
}
|
|
24
29
|
}, { isActive: active });
|
|
25
|
-
|
|
26
|
-
if (!active) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
onSubmit?.(ui.search.query);
|
|
30
|
-
}, [active, ui.search.query, onSubmit]);
|
|
31
|
-
if (!active) {
|
|
30
|
+
if (!visible) {
|
|
32
31
|
return null;
|
|
33
32
|
}
|
|
34
|
-
return (_jsxs(Box, {
|
|
33
|
+
return (_jsxs(Box, { borderColor: "white", borderStyle: "double", flexGrow: 1, children: [_jsx(Text, { dimColor: true, children: "FILTER WORKFLOWS:\u00A0" }), _jsx(Text, { children: ui.search.query }), active && _jsx(Text, { inverse: true, children: ' ' })] }));
|
|
35
34
|
};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type Tab } from '#views/dev/state/ui_state.js';
|
|
3
|
+
export declare const getHeight: () => number;
|
|
4
|
+
export interface TabBarItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
3
8
|
export declare const TabBar: React.FC<{
|
|
4
|
-
active: Tab;
|
|
9
|
+
active: Tab | string;
|
|
10
|
+
items?: readonly TabBarItem[];
|
|
11
|
+
borderColor?: string;
|
|
5
12
|
}>;
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { TAB_LABELS, TAB_ORDER } from '#views/dev/state/ui_state.js';
|
|
4
|
-
|
|
4
|
+
const TAB_LABEL_ROWS = 1;
|
|
5
|
+
const TAB_BORDER_BOTTOM_ROWS = 1;
|
|
6
|
+
export const getHeight = () => TAB_LABEL_ROWS + TAB_BORDER_BOTTOM_ROWS;
|
|
7
|
+
const DEFAULT_ITEMS = TAB_ORDER.map(tab => ({
|
|
8
|
+
id: tab,
|
|
9
|
+
label: TAB_LABELS[tab]
|
|
10
|
+
}));
|
|
11
|
+
export const TabBar = ({ active, items = DEFAULT_ITEMS, borderColor }) => (_jsx(Box, { flexDirection: "row", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: true, borderColor: borderColor ?? 'blackBright', gap: 3, children: items.map(tab => {
|
|
12
|
+
const activeTab = tab.id === active;
|
|
13
|
+
const content = _jsxs(_Fragment, { children: ["\u00A0", tab.label, "\u00A0"] });
|
|
14
|
+
if (activeTab) {
|
|
15
|
+
return (_jsx(Box, { children: _jsx(Text, { inverse: true, bold: true, children: content }) }, tab.id));
|
|
16
|
+
}
|
|
17
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: content }) }, tab.id));
|
|
18
|
+
}) }));
|
|
@@ -3,6 +3,10 @@ import { useEffect } from 'react';
|
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { useUiState } from '#views/dev/state/ui_state.js';
|
|
5
5
|
const TOAST_TTL_MS = 4000;
|
|
6
|
+
export const useHeight = () => {
|
|
7
|
+
const ui = useUiState();
|
|
8
|
+
return ui.toasts.length > 0 ? ui.toasts.length : 0;
|
|
9
|
+
};
|
|
6
10
|
const toneColor = (tone) => {
|
|
7
11
|
if (tone === 'success') {
|
|
8
12
|
return 'green';
|
|
@@ -14,12 +18,12 @@ const toneColor = (tone) => {
|
|
|
14
18
|
};
|
|
15
19
|
const tonePrefix = (tone) => {
|
|
16
20
|
if (tone === 'success') {
|
|
17
|
-
return '
|
|
21
|
+
return '[+]';
|
|
18
22
|
}
|
|
19
23
|
if (tone === 'error') {
|
|
20
|
-
return '
|
|
24
|
+
return '[!]';
|
|
21
25
|
}
|
|
22
|
-
return '
|
|
26
|
+
return '[i]';
|
|
23
27
|
};
|
|
24
28
|
export const Toasts = () => {
|
|
25
29
|
const ui = useUiState();
|
|
@@ -36,5 +40,5 @@ export const Toasts = () => {
|
|
|
36
40
|
if (toasts.length === 0) {
|
|
37
41
|
return null;
|
|
38
42
|
}
|
|
39
|
-
return (_jsx(Box, { flexDirection: "column",
|
|
43
|
+
return (_jsx(Box, { flexDirection: "column", flexGrow: 1, children: toasts.map(toast => (_jsx(Box, { backgroundColor: toneColor(toast.tone), flexGrow: 1, children: _jsxs(Text, { color: "black", wrap: "truncate-end", children: ["\u00A0", tonePrefix(toast.tone), "\u00A0", toast.message, "\u00A0"] }) }, toast.id))) }));
|
|
40
44
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const TITLE_ROWS = 1;
|
|
4
|
+
const TITLE_MARGIN_BOTTOM = 1;
|
|
5
|
+
export const getHeight = () => TITLE_ROWS + TITLE_MARGIN_BOTTOM;
|
|
6
|
+
/** Renders a bold title for panel content sections. */
|
|
7
|
+
export const ContentTitle = ({ title }) => (_jsx(Box, { marginBottom: TITLE_MARGIN_BOTTOM, children: _jsx(Text, { bold: true, children: title }) }));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StatusDisplay {
|
|
3
|
+
icon: string;
|
|
4
|
+
color: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const resolveDockerServiceStatus: (status: string) => StatusDisplay;
|
|
7
|
+
export declare const dockerServiceStatusColor: (status: string) => string;
|
|
8
|
+
/** Renders Docker service health/state without workflow status semantics. */
|
|
9
|
+
export declare const DockerServiceStatusIcon: React.FC<{
|
|
10
|
+
status: string;
|
|
11
|
+
}>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const DOCKER_SERVICE_STATUS_MAP = {
|
|
4
|
+
healthy: { icon: '●', color: 'green' },
|
|
5
|
+
unhealthy: { icon: '○', color: 'red' },
|
|
6
|
+
starting: { icon: '◐', color: 'yellow' },
|
|
7
|
+
none: { icon: '●', color: 'blue' },
|
|
8
|
+
running: { icon: '●', color: 'green' },
|
|
9
|
+
created: { icon: '◐', color: 'yellow' },
|
|
10
|
+
exited: { icon: '✗', color: 'red' }
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
|
|
13
|
+
export const resolveDockerServiceStatus = (status) => DOCKER_SERVICE_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
|
|
14
|
+
export const dockerServiceStatusColor = (status) => resolveDockerServiceStatus(status).color;
|
|
15
|
+
/** Renders Docker service health/state without workflow status semantics. */
|
|
16
|
+
export const DockerServiceStatusIcon = ({ status }) => {
|
|
17
|
+
const { icon, color } = resolveDockerServiceStatus(status);
|
|
18
|
+
return _jsx(Text, { color: color, children: icon });
|
|
19
|
+
};
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
interface DetailRenderInfo {
|
|
3
|
+
detailRows: number;
|
|
4
|
+
}
|
|
3
5
|
/**
|
|
4
6
|
* Generic two-pane shell shared by every panel that has a windowed list
|
|
5
|
-
* on top
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* separator, footer) so it lives in one place.
|
|
7
|
+
* on top and a detail pane below. Panels keep their own selection state
|
|
8
|
+
* and detail rendering; the shell owns the layout invariant (windowing,
|
|
9
|
+
* overflow indicators, separator) so it lives in one place.
|
|
9
10
|
*/
|
|
10
11
|
export interface MasterDetailPanelProps<T> {
|
|
11
12
|
items: T[];
|
|
12
13
|
selectedIndex: number;
|
|
14
|
+
height?: number;
|
|
13
15
|
visibleRows: number;
|
|
14
16
|
renderHeader: () => React.ReactNode;
|
|
15
17
|
renderRow: (item: T, selected: boolean, absoluteIndex: number) => React.ReactNode;
|
|
16
18
|
rowKey: (item: T, absoluteIndex: number) => string;
|
|
17
|
-
detail: React.ReactNode;
|
|
18
|
-
hints: CommandHint[];
|
|
19
|
-
itemLabel: string;
|
|
19
|
+
detail: React.ReactNode | ((info: DetailRenderInfo) => React.ReactNode);
|
|
20
20
|
}
|
|
21
21
|
export declare const MasterDetailPanel: <T extends object>(props: MasterDetailPanelProps<T>) => React.ReactElement;
|
|
22
|
+
export {};
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { Footer } from '#views/dev/chrome/footer.js';
|
|
5
|
-
import { HorizontalRule } from '#views/dev/chrome/divider.js';
|
|
6
4
|
import { computeWindowStart } from '#views/dev/utils/panel_helpers.js';
|
|
5
|
+
const HEADER_ROWS = 1;
|
|
6
|
+
const DETAIL_BORDER_ROWS = 2;
|
|
7
7
|
const OverflowIndicator = ({ direction, count }) => (_jsxs(Text, { dimColor: true, children: [" ", direction === 'up' ? '↑' : '↓', " ", count, " more ", direction === 'up' ? 'above' : 'below'] }));
|
|
8
8
|
export const MasterDetailPanel = (props) => {
|
|
9
|
-
const { items, selectedIndex, visibleRows, renderHeader, renderRow, rowKey, detail
|
|
9
|
+
const { items, selectedIndex, height, visibleRows, renderHeader, renderRow, rowKey, detail } = props;
|
|
10
10
|
const windowStart = computeWindowStart(selectedIndex, items.length, visibleRows);
|
|
11
11
|
const visible = items.slice(windowStart, windowStart + visibleRows);
|
|
12
12
|
const overflowAbove = windowStart;
|
|
13
13
|
const overflowBelow = items.length - (windowStart + visible.length);
|
|
14
|
-
|
|
14
|
+
const listRows = HEADER_ROWS + visible.length + (overflowAbove > 0 ? 1 : 0) + (overflowBelow > 0 ? 1 : 0);
|
|
15
|
+
const detailRows = Math.max(1, (height ?? 1) - listRows - DETAIL_BORDER_ROWS);
|
|
16
|
+
const renderedDetail = typeof detail === 'function' ? detail({ detailRows }) : detail;
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [renderHeader(), overflowAbove > 0 && _jsx(OverflowIndicator, { direction: "up", count: overflowAbove }), visible.map((item, i) => {
|
|
15
18
|
const absoluteIndex = windowStart + i;
|
|
16
19
|
return (_jsx(React.Fragment, { children: renderRow(item, absoluteIndex === selectedIndex, absoluteIndex) }, rowKey(item, absoluteIndex)));
|
|
17
|
-
}), overflowBelow > 0 && _jsx(OverflowIndicator, { direction: "down", count: overflowBelow })] }), _jsx(Box, {
|
|
20
|
+
}), overflowBelow > 0 && _jsx(OverflowIndicator, { direction: "down", count: overflowBelow })] }), _jsx(Box, { borderStyle: "single", borderColor: "blackBright", paddingX: 1, flexGrow: 1, children: renderedDetail })] }));
|
|
18
21
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { WorkflowStatusIcon, workflowStatusColor } from '#views/dev/components/workflow_status.js';
|
|
4
|
+
import { elapsedMs, formatDate, formatDurationCompact } from '#utils/date_formatter.js';
|
|
5
|
+
const SidebarKV = ({ label, value }) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { dimColor: true, bold: true, children: [label, ":\u00A0"] }), _jsx(Text, { bold: true, wrap: "wrap", children: value })] }));
|
|
6
|
+
export const RunInfoSidebar = ({ run, resultStatus, maxRows }) => {
|
|
7
|
+
const status = resultStatus ?? run.status ?? 'unknown';
|
|
8
|
+
const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
|
|
9
|
+
const rows = [
|
|
10
|
+
_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "RUN STATUS\u00A0" }), _jsx(WorkflowStatusIcon, { status: status }), _jsx(Text, { children: "\u00A0" }), _jsx(Text, { bold: true, color: workflowStatusColor(status), children: status.toUpperCase() })] }, "status"),
|
|
11
|
+
_jsx(SidebarKV, { label: "RUN ID", value: run.runId ?? '-' }, "run-id"),
|
|
12
|
+
_jsx(SidebarKV, { label: "WORKFLOW ID", value: run.workflowId ?? '-' }, "workflow-id"),
|
|
13
|
+
_jsx(SidebarKV, { label: "TYPE", value: run.workflowType ?? '-' }, "type"),
|
|
14
|
+
_jsx(SidebarKV, { label: "DURATION", value: duration }, "duration"),
|
|
15
|
+
_jsx(SidebarKV, { label: "START", value: formatDate(run.startedAt) }, "start"),
|
|
16
|
+
_jsx(SidebarKV, { label: "END", value: run.completedAt ? formatDate(run.completedAt) : '' }, "end")
|
|
17
|
+
];
|
|
18
|
+
return (_jsx(Box, { flexDirection: "column", gap: maxRows === undefined ? 1 : 0, children: rows.slice(0, maxRows) }));
|
|
19
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StatusDisplay {
|
|
3
|
+
icon: string;
|
|
4
|
+
color: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const resolveWorkflowStatus: (status: string) => StatusDisplay;
|
|
7
|
+
export declare const workflowStatusColor: (status: string) => string;
|
|
8
|
+
/** Renders workflow, run, and step status without Docker status semantics. */
|
|
9
|
+
export declare const WorkflowStatusIcon: React.FC<{
|
|
10
|
+
status: string;
|
|
11
|
+
}>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const WORKFLOW_STATUS_MAP = {
|
|
4
|
+
running: { icon: '●', color: 'yellow' },
|
|
5
|
+
completed: { icon: '●', color: 'green' },
|
|
6
|
+
failed: { icon: '✗', color: 'red' },
|
|
7
|
+
canceled: { icon: '○', color: 'gray' },
|
|
8
|
+
terminated: { icon: '✗', color: 'gray' },
|
|
9
|
+
timed_out: { icon: '✗', color: 'red' },
|
|
10
|
+
continued: { icon: '↻', color: 'blue' }
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
|
|
13
|
+
export const resolveWorkflowStatus = (status) => WORKFLOW_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
|
|
14
|
+
export const workflowStatusColor = (status) => resolveWorkflowStatus(status).color;
|
|
15
|
+
/** Renders workflow, run, and step status without Docker status semantics. */
|
|
16
|
+
export const WorkflowStatusIcon = ({ status }) => {
|
|
17
|
+
const { icon, color } = resolveWorkflowStatus(status);
|
|
18
|
+
return _jsx(Text, { color: color, children: icon });
|
|
19
|
+
};
|