@j-256/ccam 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/auth/browser-login.d.ts +14 -0
- package/dist/auth/browser-login.js +72 -0
- package/dist/auth/manual-login.d.ts +10 -0
- package/dist/auth/manual-login.js +33 -0
- package/dist/auth/paths.d.ts +4 -0
- package/dist/auth/paths.js +15 -0
- package/dist/auth/profile-resolver.d.ts +26 -0
- package/dist/auth/profile-resolver.js +42 -0
- package/dist/auth/profile-store.d.ts +38 -0
- package/dist/auth/profile-store.js +125 -0
- package/dist/auth/prompt.d.ts +9 -0
- package/dist/auth/prompt.js +70 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +4 -0
- package/dist/client-factory.d.ts +6 -0
- package/dist/client-factory.js +40 -0
- package/dist/commands/auth.d.ts +77 -0
- package/dist/commands/auth.js +387 -0
- package/dist/commands/client.d.ts +3 -0
- package/dist/commands/client.js +365 -0
- package/dist/commands/instance.d.ts +11 -0
- package/dist/commands/instance.js +128 -0
- package/dist/commands/org-config.d.ts +3 -0
- package/dist/commands/org-config.js +31 -0
- package/dist/commands/org.d.ts +11 -0
- package/dist/commands/org.js +234 -0
- package/dist/commands/permission.d.ts +3 -0
- package/dist/commands/permission.js +60 -0
- package/dist/commands/realm.d.ts +3 -0
- package/dist/commands/realm.js +58 -0
- package/dist/commands/role.d.ts +3 -0
- package/dist/commands/role.js +77 -0
- package/dist/commands/service-type.d.ts +3 -0
- package/dist/commands/service-type.js +57 -0
- package/dist/commands/user.d.ts +14 -0
- package/dist/commands/user.js +573 -0
- package/dist/error-handler.d.ts +2 -0
- package/dist/error-handler.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/output/csv.d.ts +3 -0
- package/dist/output/csv.js +57 -0
- package/dist/output/default-columns.d.ts +2 -0
- package/dist/output/default-columns.js +16 -0
- package/dist/output/detect.d.ts +4 -0
- package/dist/output/detect.js +6 -0
- package/dist/output/index.d.ts +15 -0
- package/dist/output/index.js +34 -0
- package/dist/output/json.d.ts +2 -0
- package/dist/output/json.js +10 -0
- package/dist/output/shared.d.ts +13 -0
- package/dist/output/shared.js +41 -0
- package/dist/output/table.d.ts +2 -0
- package/dist/output/table.js +72 -0
- package/dist/output/types.d.ts +2 -0
- package/dist/output/types.js +2 -0
- package/dist/output/yaml-fmt.d.ts +2 -0
- package/dist/output/yaml-fmt.js +11 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +37 -0
- package/dist/shared.d.ts +46 -0
- package/dist/shared.js +96 -0
- package/dist/tui/App.d.ts +7 -0
- package/dist/tui/App.js +30 -0
- package/dist/tui/components/AuditTab.d.ts +8 -0
- package/dist/tui/components/AuditTab.js +80 -0
- package/dist/tui/components/FooterBar.d.ts +17 -0
- package/dist/tui/components/FooterBar.js +23 -0
- package/dist/tui/components/FullScreenLayout.d.ts +6 -0
- package/dist/tui/components/FullScreenLayout.js +11 -0
- package/dist/tui/components/HeaderBar.d.ts +5 -0
- package/dist/tui/components/HeaderBar.js +10 -0
- package/dist/tui/components/InfoTab.d.ts +8 -0
- package/dist/tui/components/InfoTab.js +70 -0
- package/dist/tui/components/ResourcePicker.d.ts +10 -0
- package/dist/tui/components/ResourcePicker.js +36 -0
- package/dist/tui/components/SubResourceTab.d.ts +7 -0
- package/dist/tui/components/SubResourceTab.js +193 -0
- package/dist/tui/components/TabBar.d.ts +11 -0
- package/dist/tui/components/TabBar.js +13 -0
- package/dist/tui/components/Table.d.ts +32 -0
- package/dist/tui/components/Table.js +175 -0
- package/dist/tui/context/client.d.ts +7 -0
- package/dist/tui/context/client.js +14 -0
- package/dist/tui/context/navigation.d.ts +14 -0
- package/dist/tui/context/navigation.js +25 -0
- package/dist/tui/context/terminal-size.d.ts +9 -0
- package/dist/tui/context/terminal-size.js +26 -0
- package/dist/tui/format.d.ts +20 -0
- package/dist/tui/format.js +57 -0
- package/dist/tui/hooks/use-audit-log.d.ts +12 -0
- package/dist/tui/hooks/use-audit-log.js +71 -0
- package/dist/tui/hooks/use-local-collection.d.ts +8 -0
- package/dist/tui/hooks/use-local-collection.js +30 -0
- package/dist/tui/hooks/use-paginated-resource.d.ts +23 -0
- package/dist/tui/hooks/use-paginated-resource.js +115 -0
- package/dist/tui/hooks/use-resource-detail.d.ts +7 -0
- package/dist/tui/hooks/use-resource-detail.js +30 -0
- package/dist/tui/hooks/use-scroll-window.d.ts +7 -0
- package/dist/tui/hooks/use-scroll-window.js +29 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +22 -0
- package/dist/tui/navigation.d.ts +11 -0
- package/dist/tui/navigation.js +29 -0
- package/dist/tui/resource-configs/api-clients.d.ts +3 -0
- package/dist/tui/resource-configs/api-clients.js +118 -0
- package/dist/tui/resource-configs/index.d.ts +14 -0
- package/dist/tui/resource-configs/index.js +28 -0
- package/dist/tui/resource-configs/instances.d.ts +3 -0
- package/dist/tui/resource-configs/instances.js +24 -0
- package/dist/tui/resource-configs/org-configuration.d.ts +3 -0
- package/dist/tui/resource-configs/org-configuration.js +28 -0
- package/dist/tui/resource-configs/organizations.d.ts +3 -0
- package/dist/tui/resource-configs/organizations.js +104 -0
- package/dist/tui/resource-configs/permissions.d.ts +3 -0
- package/dist/tui/resource-configs/permissions.js +25 -0
- package/dist/tui/resource-configs/realms.d.ts +3 -0
- package/dist/tui/resource-configs/realms.js +36 -0
- package/dist/tui/resource-configs/roles.d.ts +3 -0
- package/dist/tui/resource-configs/roles.js +56 -0
- package/dist/tui/resource-configs/service-types.d.ts +3 -0
- package/dist/tui/resource-configs/service-types.js +24 -0
- package/dist/tui/resource-configs/users.d.ts +3 -0
- package/dist/tui/resource-configs/users.js +126 -0
- package/dist/tui/types.d.ts +99 -0
- package/dist/tui/types.js +23 -0
- package/dist/tui/views/ResourceDetailView.d.ts +7 -0
- package/dist/tui/views/ResourceDetailView.js +123 -0
- package/dist/tui/views/ResourceListView.d.ts +6 -0
- package/dist/tui/views/ResourceListView.js +140 -0
- package/dist/tui/views/ViewRouter.d.ts +2 -0
- package/dist/tui/views/ViewRouter.js +60 -0
- package/package.json +62 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FieldDef, CrossLinkDef } from '../types.js';
|
|
2
|
+
export interface InfoTabProps {
|
|
3
|
+
fields: FieldDef[];
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
crossLinks: CrossLinkDef[];
|
|
6
|
+
}
|
|
7
|
+
export declare function InfoTab({ fields, data, crossLinks }: InfoTabProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=InfoTab.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { useNav } from '../context/navigation.js';
|
|
5
|
+
import { formatCell } from './Table.js';
|
|
6
|
+
const LABEL_WIDTH = 24;
|
|
7
|
+
export function InfoTab({ fields, data, crossLinks }) {
|
|
8
|
+
const nav = useNav();
|
|
9
|
+
// Build list of navigable cross-link indices
|
|
10
|
+
const crossLinkIndices = useMemo(() => {
|
|
11
|
+
const indices = [];
|
|
12
|
+
for (let i = 0; i < fields.length; i++) {
|
|
13
|
+
const field = fields[i];
|
|
14
|
+
if (field.crossLink || crossLinks.some((cl) => cl.field === field.key)) {
|
|
15
|
+
indices.push(i);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return indices;
|
|
19
|
+
}, [fields, crossLinks]);
|
|
20
|
+
const [highlightPos, setHighlightPos] = useState(0);
|
|
21
|
+
const highlightIndex = crossLinkIndices.length > 0
|
|
22
|
+
? crossLinkIndices[highlightPos]
|
|
23
|
+
: -1;
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (crossLinkIndices.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
if (input === 'j' || key.downArrow) {
|
|
28
|
+
setHighlightPos((i) => Math.min(i + 1, crossLinkIndices.length - 1));
|
|
29
|
+
}
|
|
30
|
+
if (input === 'k' || key.upArrow) {
|
|
31
|
+
setHighlightPos((i) => Math.max(i - 1, 0));
|
|
32
|
+
}
|
|
33
|
+
if (key.return) {
|
|
34
|
+
const fieldIdx = crossLinkIndices[highlightPos];
|
|
35
|
+
if (fieldIdx == null)
|
|
36
|
+
return;
|
|
37
|
+
const field = fields[fieldIdx];
|
|
38
|
+
const cl = field.crossLink || crossLinks.find((c) => c.field === field.key);
|
|
39
|
+
if (!cl)
|
|
40
|
+
return;
|
|
41
|
+
const targetId = String(data[cl.field] ?? '');
|
|
42
|
+
if (targetId) {
|
|
43
|
+
nav.push({
|
|
44
|
+
view: cl.targetView,
|
|
45
|
+
label: String(data[cl.field] ?? targetId),
|
|
46
|
+
params: { id: targetId },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const hasCrossLinks = crossLinkIndices.length > 0;
|
|
52
|
+
const MAX_VALUE_WIDTH = 80;
|
|
53
|
+
const rows = [];
|
|
54
|
+
let prevGroup;
|
|
55
|
+
for (let i = 0; i < fields.length; i++) {
|
|
56
|
+
const field = fields[i];
|
|
57
|
+
// Insert blank line when group changes (skip before the very first field)
|
|
58
|
+
if (field.group && field.group !== prevGroup && i > 0) {
|
|
59
|
+
rows.push(_jsx(Box, { height: 1 }, `sep-${i}`));
|
|
60
|
+
}
|
|
61
|
+
prevGroup = field.group;
|
|
62
|
+
const value = data[field.key];
|
|
63
|
+
const raw = field.format ? field.format(value) : undefined;
|
|
64
|
+
const formatted = raw ?? formatCell(value, MAX_VALUE_WIDTH);
|
|
65
|
+
const isCrossLinked = i === highlightIndex && hasCrossLinks;
|
|
66
|
+
rows.push(_jsxs(Box, { children: [hasCrossLinks && (_jsx(Box, { width: 2, children: _jsx(Text, { color: "yellow", bold: true, children: isCrossLinked ? '\u25b8 ' : ' ' }) })), _jsx(Box, { width: LABEL_WIDTH, children: _jsx(Text, { dimColor: true, children: field.label }) }), _jsx(Text, { bold: isCrossLinked, color: isCrossLinked ? 'cyan' : undefined, children: formatted })] }, field.key));
|
|
67
|
+
}
|
|
68
|
+
return (_jsx(Box, { flexDirection: "column", children: rows }));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=InfoTab.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ViewEntry } from '../types.js';
|
|
2
|
+
interface ResourceItem {
|
|
3
|
+
label: string;
|
|
4
|
+
description: string;
|
|
5
|
+
entry: ViewEntry;
|
|
6
|
+
}
|
|
7
|
+
declare const RESOURCES: ResourceItem[];
|
|
8
|
+
export { RESOURCES };
|
|
9
|
+
export declare function ResourcePicker(): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=ResourcePicker.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { useNav } from '../context/navigation.js';
|
|
5
|
+
const RESOURCES = [
|
|
6
|
+
{ label: 'Organizations', description: 'Customer orgs, realms, and instances', entry: { view: 'org-list', label: 'Organizations' } },
|
|
7
|
+
{ label: 'Users', description: 'User accounts and access', entry: { view: 'user-list', label: 'Users' } },
|
|
8
|
+
{ label: 'API Clients', description: 'OAuth2 API client credentials', entry: { view: 'client-list', label: 'API Clients' } },
|
|
9
|
+
{ label: 'Roles', description: 'Role definitions and permissions', entry: { view: 'role-list', label: 'Roles' } },
|
|
10
|
+
{ label: 'Realms', description: 'Realm/tenant definitions', entry: { view: 'realm-list', label: 'Realms' } },
|
|
11
|
+
{ label: 'Instances', description: 'Commerce Cloud instances', entry: { view: 'instance-list', label: 'Instances' } },
|
|
12
|
+
{ label: 'Permissions', description: 'Permission definitions', entry: { view: 'permission-list', label: 'Permissions' } },
|
|
13
|
+
{ label: 'Service Types', description: 'Service type definitions', entry: { view: 'service-type-list', label: 'Service Types' } },
|
|
14
|
+
{ label: 'Organization Configuration', description: 'Org-level configuration', entry: { view: 'org-config-detail', label: 'Org Configuration' } },
|
|
15
|
+
];
|
|
16
|
+
export { RESOURCES };
|
|
17
|
+
export function ResourcePicker() {
|
|
18
|
+
const [index, setIndex] = useState(0);
|
|
19
|
+
const nav = useNav();
|
|
20
|
+
useInput((input, key) => {
|
|
21
|
+
if (input === 'j' || key.downArrow) {
|
|
22
|
+
setIndex((i) => Math.min(i + 1, RESOURCES.length - 1));
|
|
23
|
+
}
|
|
24
|
+
if (input === 'k' || key.upArrow) {
|
|
25
|
+
setIndex((i) => Math.max(i - 1, 0));
|
|
26
|
+
}
|
|
27
|
+
if (key.return) {
|
|
28
|
+
nav.push(RESOURCES[index].entry);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select a resource" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: RESOURCES.map((r, i) => {
|
|
32
|
+
const selected = i === index;
|
|
33
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", bold: true, children: selected ? '\u25b8 ' : ' ' }), _jsx(Text, { bold: selected, color: selected ? 'white' : 'cyan', children: r.label }), _jsxs(Text, { dimColor: true, children: [" ", r.description] })] }, r.label));
|
|
34
|
+
}) })] }));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=ResourcePicker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LocalTabConfig, type PaginatedTabConfig } from '../types.js';
|
|
2
|
+
export interface SubResourceTabProps {
|
|
3
|
+
tab: LocalTabConfig | PaginatedTabConfig;
|
|
4
|
+
parentId: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function SubResourceTab({ tab, parentId }: SubResourceTabProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=SubResourceTab.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { useClient } from '../context/client.js';
|
|
6
|
+
import { useNav } from '../context/navigation.js';
|
|
7
|
+
import { useLocalCollection } from '../hooks/use-local-collection.js';
|
|
8
|
+
import { usePaginatedResource } from '../hooks/use-paginated-resource.js';
|
|
9
|
+
import { useScrollWindow } from '../hooks/use-scroll-window.js';
|
|
10
|
+
import { useTerminalSize } from '../context/terminal-size.js';
|
|
11
|
+
import { Table } from './Table.js';
|
|
12
|
+
import { getSortableColumns } from '../types.js';
|
|
13
|
+
const BORDER_INSET = 2;
|
|
14
|
+
function LocalSubResource({ tab, parentId }) {
|
|
15
|
+
const client = useClient();
|
|
16
|
+
const nav = useNav();
|
|
17
|
+
const { cols } = useTerminalSize();
|
|
18
|
+
const [highlight, setHighlight] = useState(0);
|
|
19
|
+
const fetchFn = useCallback(() => tab.fetchFn(client, parentId), [client, parentId, tab]);
|
|
20
|
+
const { data, loading, error, retry } = useLocalCollection(fetchFn);
|
|
21
|
+
const sortableColumns = getSortableColumns(tab.columns, 'local');
|
|
22
|
+
// Client-side sort state
|
|
23
|
+
const [sortKey, setSortKey] = useState(null);
|
|
24
|
+
const [sortDir, setSortDir] = useState('asc');
|
|
25
|
+
const sortedData = useMemo(() => {
|
|
26
|
+
if (!sortKey)
|
|
27
|
+
return data;
|
|
28
|
+
const sorted = [...data].sort((a, b) => {
|
|
29
|
+
const aVal = a[sortKey];
|
|
30
|
+
const bVal = b[sortKey];
|
|
31
|
+
return String(aVal ?? '').localeCompare(String(bVal ?? ''));
|
|
32
|
+
});
|
|
33
|
+
return sortDir === 'desc' ? sorted.reverse() : sorted;
|
|
34
|
+
}, [data, sortKey, sortDir]);
|
|
35
|
+
const scroll = useScrollWindow(highlight, sortedData.length);
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
if (loading)
|
|
38
|
+
return;
|
|
39
|
+
if (error) {
|
|
40
|
+
if (input === 'r')
|
|
41
|
+
retry();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Vertical movement
|
|
45
|
+
if (input === 'j' || key.downArrow) {
|
|
46
|
+
setHighlight((i) => Math.min(i + 1, sortedData.length - 1));
|
|
47
|
+
}
|
|
48
|
+
if (input === 'k' || key.upArrow) {
|
|
49
|
+
setHighlight((i) => Math.max(i - 1, 0));
|
|
50
|
+
}
|
|
51
|
+
// Jump to first/last
|
|
52
|
+
if (input === 'g')
|
|
53
|
+
setHighlight(0);
|
|
54
|
+
if (input === 'G')
|
|
55
|
+
setHighlight(Math.max(0, sortedData.length - 1));
|
|
56
|
+
// Client-side sort cycling
|
|
57
|
+
if (input === 's' && sortableColumns.length > 0) {
|
|
58
|
+
if (!sortKey) {
|
|
59
|
+
setSortKey(sortableColumns[0].field);
|
|
60
|
+
setSortDir('asc');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const idx = sortableColumns.findIndex((c) => c.field === sortKey);
|
|
64
|
+
const nextIdx = (idx + 1) % sortableColumns.length;
|
|
65
|
+
setSortKey(sortableColumns[nextIdx].field);
|
|
66
|
+
setSortDir('asc');
|
|
67
|
+
}
|
|
68
|
+
setHighlight(0);
|
|
69
|
+
}
|
|
70
|
+
if (input === 'S' && sortKey) {
|
|
71
|
+
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
72
|
+
setHighlight(0);
|
|
73
|
+
}
|
|
74
|
+
// Enter: follow cross-link
|
|
75
|
+
if (key.return && sortedData.length > 0 && tab.crossLinkTo) {
|
|
76
|
+
const item = sortedData[highlight];
|
|
77
|
+
if (item) {
|
|
78
|
+
const id = String(item.id ?? '');
|
|
79
|
+
nav.push({
|
|
80
|
+
view: tab.crossLinkTo,
|
|
81
|
+
label: String(item.id ?? item.name ?? item.description ?? ''),
|
|
82
|
+
params: { id },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Scroll
|
|
87
|
+
if (input === 'n') {
|
|
88
|
+
setHighlight((i) => Math.min(i + scroll.visibleRows, sortedData.length - 1));
|
|
89
|
+
}
|
|
90
|
+
if (input === 'p') {
|
|
91
|
+
setHighlight((i) => Math.max(i - scroll.visibleRows, 0));
|
|
92
|
+
}
|
|
93
|
+
// Refresh
|
|
94
|
+
if (input === 'r')
|
|
95
|
+
retry();
|
|
96
|
+
});
|
|
97
|
+
if (error) {
|
|
98
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error.message] }), _jsx(Text, { dimColor: true, children: "r:retry" })] }));
|
|
99
|
+
}
|
|
100
|
+
if (loading && data.length === 0) {
|
|
101
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] }));
|
|
102
|
+
}
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Table, { columns: tab.columns, data: sortedData, highlightIndex: highlight, visibleRows: scroll.visibleRows, scrollOffset: scroll.scrollOffset, contentWidth: cols - BORDER_INSET }), _jsxs(Text, { dimColor: true, children: [sortedData.length, " items", sortKey
|
|
104
|
+
? ` | Sort: ${sortableColumns.find((column) => column.field === sortKey)?.label ?? sortKey} ${sortDir}`
|
|
105
|
+
: '', sortableColumns.length > 0 ? ' | [s:sort] [S:reverse]' : ''] })] }));
|
|
106
|
+
}
|
|
107
|
+
function PaginatedSubResource({ tab, parentId }) {
|
|
108
|
+
const client = useClient();
|
|
109
|
+
const nav = useNav();
|
|
110
|
+
const { cols } = useTerminalSize();
|
|
111
|
+
const [highlight, setHighlight] = useState(0);
|
|
112
|
+
const fetchFn = useCallback((page, size) => tab.fetchFn(client, parentId, page, size), [client, parentId, tab]);
|
|
113
|
+
const { data, page, totalPages, totalElements, loading, error, paginated, nextPage, prevPage, retry, } = usePaginatedResource(fetchFn);
|
|
114
|
+
const scroll = useScrollWindow(highlight, data.length);
|
|
115
|
+
useInput((input, key) => {
|
|
116
|
+
if (loading)
|
|
117
|
+
return;
|
|
118
|
+
if (error) {
|
|
119
|
+
if (input === 'r')
|
|
120
|
+
retry();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Vertical movement
|
|
124
|
+
if (input === 'j' || key.downArrow) {
|
|
125
|
+
setHighlight((i) => Math.min(i + 1, data.length - 1));
|
|
126
|
+
}
|
|
127
|
+
if (input === 'k' || key.upArrow) {
|
|
128
|
+
setHighlight((i) => Math.max(i - 1, 0));
|
|
129
|
+
}
|
|
130
|
+
// Jump to first/last
|
|
131
|
+
if (input === 'g')
|
|
132
|
+
setHighlight(0);
|
|
133
|
+
if (input === 'G')
|
|
134
|
+
setHighlight(Math.max(0, data.length - 1));
|
|
135
|
+
// Enter: follow cross-link
|
|
136
|
+
if (key.return && data.length > 0 && tab.crossLinkTo) {
|
|
137
|
+
const item = data[highlight];
|
|
138
|
+
if (item) {
|
|
139
|
+
const id = String(item.id ?? '');
|
|
140
|
+
nav.push({
|
|
141
|
+
view: tab.crossLinkTo,
|
|
142
|
+
label: String(item.id ?? item.name ?? item.description ?? ''),
|
|
143
|
+
params: { id },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Pagination
|
|
148
|
+
if (input === 'n') {
|
|
149
|
+
if (paginated) {
|
|
150
|
+
nextPage();
|
|
151
|
+
setHighlight(0);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
setHighlight((i) => Math.min(i + scroll.visibleRows, data.length - 1));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (input === 'p') {
|
|
158
|
+
if (paginated) {
|
|
159
|
+
prevPage();
|
|
160
|
+
setHighlight(0);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
setHighlight((i) => Math.max(i - scroll.visibleRows, 0));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Refresh
|
|
167
|
+
if (input === 'r')
|
|
168
|
+
retry();
|
|
169
|
+
});
|
|
170
|
+
if (error) {
|
|
171
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error.message] }), _jsx(Text, { dimColor: true, children: "r:retry" })] }));
|
|
172
|
+
}
|
|
173
|
+
if (loading && data.length === 0) {
|
|
174
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] }));
|
|
175
|
+
}
|
|
176
|
+
let pageInfo;
|
|
177
|
+
if (paginated) {
|
|
178
|
+
const start = page * 25 + 1;
|
|
179
|
+
const end = Math.min(start + data.length - 1, totalElements);
|
|
180
|
+
pageInfo = `${start}-${end} of ${totalElements} | Page ${page + 1}/${totalPages}`;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
pageInfo = `${totalElements} items`;
|
|
184
|
+
}
|
|
185
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Table, { columns: tab.columns, data: data, highlightIndex: highlight, visibleRows: scroll.visibleRows, scrollOffset: scroll.scrollOffset, contentWidth: cols - BORDER_INSET }), _jsx(Text, { dimColor: true, children: pageInfo })] }));
|
|
186
|
+
}
|
|
187
|
+
export function SubResourceTab({ tab, parentId }) {
|
|
188
|
+
if (tab.type === 'local') {
|
|
189
|
+
return _jsx(LocalSubResource, { tab: tab, parentId: parentId });
|
|
190
|
+
}
|
|
191
|
+
return _jsx(PaginatedSubResource, { tab: tab, parentId: parentId });
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=SubResourceTab.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface TabDef {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
count?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TabBarProps {
|
|
7
|
+
tabs: TabDef[];
|
|
8
|
+
activeKey: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function TabBar({ tabs, activeKey }: TabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=TabBar.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function TabBar({ tabs, activeKey }) {
|
|
4
|
+
return (_jsx(Box, { gap: 1, children: tabs.map((tab) => {
|
|
5
|
+
const isActive = tab.key === activeKey;
|
|
6
|
+
const label = tab.count != null ? `${tab.label} (${tab.count})` : tab.label;
|
|
7
|
+
if (isActive) {
|
|
8
|
+
return (_jsx(Text, { bold: true, underline: true, color: "cyan", children: label }, tab.key));
|
|
9
|
+
}
|
|
10
|
+
return (_jsx(Text, { children: label }, tab.key));
|
|
11
|
+
}) }));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=TabBar.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ColumnDef } from '../types.js';
|
|
2
|
+
export interface TableProps {
|
|
3
|
+
columns: ColumnDef[];
|
|
4
|
+
data: Record<string, unknown>[];
|
|
5
|
+
highlightIndex: number;
|
|
6
|
+
visibleRows?: number;
|
|
7
|
+
scrollOffset?: number;
|
|
8
|
+
/** Hide the cursor marker column (used in sub-resource tabs where Enter isn't available) */
|
|
9
|
+
hideCursor?: boolean;
|
|
10
|
+
/** Key of currently sorted column */
|
|
11
|
+
sortKey?: string;
|
|
12
|
+
/** Sort direction */
|
|
13
|
+
sortDirection?: 'asc' | 'desc';
|
|
14
|
+
/** Override terminal width (e.g. when rendered inside a bordered box) */
|
|
15
|
+
contentWidth?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Strip ANSI escape codes to get visible length */
|
|
18
|
+
export declare function stripAnsi(str: string): string;
|
|
19
|
+
/** Truncate a string by visible characters while preserving ANSI styling */
|
|
20
|
+
export declare function truncateVisible(str: string, width: number): string;
|
|
21
|
+
/** Pad string to target width based on visible characters (left-align: append spaces) */
|
|
22
|
+
export declare function padVisible(str: string, width: number): string;
|
|
23
|
+
/** Pad string to target width based on visible characters (right-align: prepend spaces) */
|
|
24
|
+
export declare function padVisibleRight(str: string, width: number): string;
|
|
25
|
+
/** Pick which columns to show and compute their character widths */
|
|
26
|
+
export declare function allocateColumns(columns: ColumnDef[], terminalWidth: number, colGap?: number): {
|
|
27
|
+
col: ColumnDef;
|
|
28
|
+
charWidth: number;
|
|
29
|
+
}[];
|
|
30
|
+
export declare function formatCell(value: unknown, maxWidth: number, formatter?: (v: unknown) => string): string;
|
|
31
|
+
export declare function Table({ columns, data, highlightIndex, visibleRows, scrollOffset, hideCursor, sortKey, sortDirection, contentWidth, }: TableProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
//# sourceMappingURL=Table.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useTerminalSize } from '../context/terminal-size.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// ANSI-aware string helpers (exported for testing)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Strip ANSI escape codes to get visible length */
|
|
8
|
+
export function stripAnsi(str) {
|
|
9
|
+
// eslint-disable-next-line no-control-regex
|
|
10
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
11
|
+
}
|
|
12
|
+
/** Truncate a string by visible characters while preserving ANSI styling */
|
|
13
|
+
export function truncateVisible(str, width) {
|
|
14
|
+
if (width < 4 || stripAnsi(str).length <= width)
|
|
15
|
+
return str;
|
|
16
|
+
const target = width - 3;
|
|
17
|
+
let visible = 0;
|
|
18
|
+
let i = 0;
|
|
19
|
+
let out = '';
|
|
20
|
+
let sawAnsi = false;
|
|
21
|
+
while (i < str.length && visible < target) {
|
|
22
|
+
if (str[i] === '\x1b') {
|
|
23
|
+
// eslint-disable-next-line no-control-regex
|
|
24
|
+
const match = /^\x1b\[[0-9;]*m/.exec(str.slice(i));
|
|
25
|
+
if (match) {
|
|
26
|
+
out += match[0];
|
|
27
|
+
i += match[0].length;
|
|
28
|
+
sawAnsi = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
out += str[i];
|
|
33
|
+
i += 1;
|
|
34
|
+
visible += 1;
|
|
35
|
+
}
|
|
36
|
+
out += '...';
|
|
37
|
+
// Close any active style so it does not bleed into the rest of the row.
|
|
38
|
+
if (sawAnsi) {
|
|
39
|
+
out += '\x1b[0m';
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Pad string to target width based on visible characters (left-align: append spaces) */
|
|
44
|
+
export function padVisible(str, width) {
|
|
45
|
+
const visible = stripAnsi(str).length;
|
|
46
|
+
if (visible >= width)
|
|
47
|
+
return str;
|
|
48
|
+
return str + ' '.repeat(width - visible);
|
|
49
|
+
}
|
|
50
|
+
/** Pad string to target width based on visible characters (right-align: prepend spaces) */
|
|
51
|
+
export function padVisibleRight(str, width) {
|
|
52
|
+
const visible = stripAnsi(str).length;
|
|
53
|
+
if (visible >= width)
|
|
54
|
+
return str;
|
|
55
|
+
return ' '.repeat(width - visible) + str;
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Column allocation helpers (exported for testing)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const DEFAULT_MIN_WIDTH = 4;
|
|
61
|
+
const DEFAULT_PRIORITY = 10;
|
|
62
|
+
const BASE_FG = '#e6edf3';
|
|
63
|
+
const ROW_BG = '#0a0d12';
|
|
64
|
+
const ZEBRA_BG = '#10151b';
|
|
65
|
+
/** Pick which columns to show and compute their character widths */
|
|
66
|
+
export function allocateColumns(columns, terminalWidth, colGap = 2) {
|
|
67
|
+
if (columns.length === 0)
|
|
68
|
+
return [];
|
|
69
|
+
// Sort candidates by priority ascending (lower = hidden first)
|
|
70
|
+
const candidates = columns.map((col) => ({
|
|
71
|
+
col,
|
|
72
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_WIDTH,
|
|
73
|
+
priority: col.priority ?? DEFAULT_PRIORITY,
|
|
74
|
+
}));
|
|
75
|
+
// Iteratively remove lowest-priority columns until the remaining fit
|
|
76
|
+
const visible = [...candidates];
|
|
77
|
+
while (visible.length > 0) {
|
|
78
|
+
const totalGap = colGap * (visible.length - 1);
|
|
79
|
+
const totalMinWidth = visible.reduce((s, c) => s + c.minWidth, 0);
|
|
80
|
+
if (totalMinWidth + totalGap <= terminalWidth)
|
|
81
|
+
break;
|
|
82
|
+
// Remove the column with the lowest priority (ties: last in array)
|
|
83
|
+
let worstIdx = 0;
|
|
84
|
+
let worstPriority = visible[0].priority;
|
|
85
|
+
for (let i = 1; i < visible.length; i++) {
|
|
86
|
+
if (visible[i].priority < worstPriority) {
|
|
87
|
+
worstPriority = visible[i].priority;
|
|
88
|
+
worstIdx = i;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
visible.splice(worstIdx, 1);
|
|
92
|
+
}
|
|
93
|
+
if (visible.length === 0)
|
|
94
|
+
return [];
|
|
95
|
+
// Proportional allocation of remaining width
|
|
96
|
+
const totalGap = colGap * (visible.length - 1);
|
|
97
|
+
const availableWidth = terminalWidth - totalGap;
|
|
98
|
+
const totalProportion = visible.reduce((s, c) => s + c.col.width, 0);
|
|
99
|
+
return visible.map((c) => {
|
|
100
|
+
const raw = Math.floor((c.col.width / totalProportion) * availableWidth);
|
|
101
|
+
const charWidth = Math.max(raw, c.minWidth);
|
|
102
|
+
return { col: c.col, charWidth };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Cell formatting
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
export function formatCell(value, maxWidth, formatter) {
|
|
109
|
+
let str;
|
|
110
|
+
if (formatter) {
|
|
111
|
+
str = formatter(value);
|
|
112
|
+
}
|
|
113
|
+
else if (value === null || value === undefined) {
|
|
114
|
+
str = '-';
|
|
115
|
+
}
|
|
116
|
+
else if (Array.isArray(value)) {
|
|
117
|
+
str = value.join(', ');
|
|
118
|
+
}
|
|
119
|
+
else if (typeof value === 'object') {
|
|
120
|
+
str = JSON.stringify(value);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
str = String(value);
|
|
124
|
+
}
|
|
125
|
+
return truncateVisible(str, maxWidth);
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Table component
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
const CURSOR_WIDTH = 2; // "▸ " or " "
|
|
131
|
+
export function Table({ columns, data, highlightIndex, visibleRows, scrollOffset, hideCursor, sortKey, sortDirection, contentWidth, }) {
|
|
132
|
+
const { cols } = useTerminalSize();
|
|
133
|
+
const effectiveWidth = contentWidth ?? cols;
|
|
134
|
+
if (data.length === 0) {
|
|
135
|
+
return _jsx(Text, { dimColor: true, children: "No data" });
|
|
136
|
+
}
|
|
137
|
+
const showCursor = !hideCursor;
|
|
138
|
+
const colGap = 1;
|
|
139
|
+
const tableWidth = showCursor ? effectiveWidth - CURSOR_WIDTH : effectiveWidth;
|
|
140
|
+
const allocated = allocateColumns(columns, tableWidth, colGap);
|
|
141
|
+
if (allocated.length === 0) {
|
|
142
|
+
return _jsx(Text, { dimColor: true, children: "Terminal too narrow" });
|
|
143
|
+
}
|
|
144
|
+
const offset = scrollOffset ?? 0;
|
|
145
|
+
const displayData = visibleRows != null ? data.slice(offset, offset + visibleRows) : data;
|
|
146
|
+
const adjustedHighlight = highlightIndex - offset;
|
|
147
|
+
// Separator line width = sum of column widths + gaps
|
|
148
|
+
const separatorWidth = allocated.reduce((s, a) => s + a.charWidth, 0) +
|
|
149
|
+
colGap * (allocated.length - 1);
|
|
150
|
+
const separator = '\u2500'.repeat(separatorWidth);
|
|
151
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [showCursor && _jsx(Box, { width: CURSOR_WIDTH, children: _jsx(Text, { children: " " }) }), allocated.map((a, i) => {
|
|
152
|
+
let label = a.col.label;
|
|
153
|
+
const sortField = a.col.sort?.field;
|
|
154
|
+
if (sortKey && (a.col.key === sortKey || sortField === sortKey)) {
|
|
155
|
+
label += sortDirection === 'desc' ? ' \u25bc' : ' \u25b2';
|
|
156
|
+
}
|
|
157
|
+
return (_jsx(Box, { width: a.charWidth, marginRight: i < allocated.length - 1 ? colGap : 0, children: _jsx(Text, { bold: true, color: a.col.color ?? BASE_FG, children: formatCell(label, a.charWidth) }) }, a.col.key));
|
|
158
|
+
})] }), _jsx(Text, { dimColor: true, children: showCursor ? ' ' + separator : separator }), displayData.map((row, i) => {
|
|
159
|
+
const isHighlighted = i === adjustedHighlight;
|
|
160
|
+
const isZebra = (offset + i) % 2 === 1;
|
|
161
|
+
return (_jsxs(Box, { width: effectiveWidth, backgroundColor: isZebra ? ZEBRA_BG : ROW_BG, children: [showCursor && (_jsx(Box, { width: CURSOR_WIDTH, children: _jsx(Text, { color: isHighlighted ? 'yellow' : undefined, bold: isHighlighted, children: isHighlighted ? '\u25b8 ' : ' ' }) })), allocated.map((a, j) => {
|
|
162
|
+
let cellStr = formatCell(row[a.col.key], a.charWidth, a.col.format);
|
|
163
|
+
// Highlighted rows force a uniform foreground so formatter ANSI
|
|
164
|
+
// colors do not reduce contrast against the selection styling.
|
|
165
|
+
if (isHighlighted) {
|
|
166
|
+
cellStr = stripAnsi(cellStr);
|
|
167
|
+
}
|
|
168
|
+
cellStr = a.col.align === 'right'
|
|
169
|
+
? padVisibleRight(cellStr, a.charWidth)
|
|
170
|
+
: padVisible(cellStr, a.charWidth);
|
|
171
|
+
return (_jsx(Box, { width: a.charWidth, marginRight: j < allocated.length - 1 ? colGap : 0, children: _jsx(Text, { bold: isHighlighted || j === 0, color: isHighlighted ? 'white' : (a.col.color ?? BASE_FG), children: cellStr }) }, a.col.key));
|
|
172
|
+
})] }, offset + i));
|
|
173
|
+
})] }));
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=Table.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CcamClient } from 'ccam-sdk';
|
|
2
|
+
export declare function ClientProvider({ client, children, }: {
|
|
3
|
+
client: CcamClient;
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function useClient(): CcamClient;
|
|
7
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
const ClientContext = createContext(null);
|
|
4
|
+
export function ClientProvider({ client, children, }) {
|
|
5
|
+
return _jsx(ClientContext.Provider, { value: client, children: children });
|
|
6
|
+
}
|
|
7
|
+
export function useClient() {
|
|
8
|
+
const client = useContext(ClientContext);
|
|
9
|
+
if (!client) {
|
|
10
|
+
throw new Error('useClient must be used within a ClientProvider');
|
|
11
|
+
}
|
|
12
|
+
return client;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ViewEntry } from '../types.js';
|
|
2
|
+
export interface NavContext {
|
|
3
|
+
current: ViewEntry;
|
|
4
|
+
breadcrumbs: string[];
|
|
5
|
+
canGoBack: boolean;
|
|
6
|
+
push: (entry: ViewEntry) => void;
|
|
7
|
+
pop: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function NavigationProvider({ initial, children, }: {
|
|
10
|
+
initial: ViewEntry;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare function useNav(): NavContext;
|
|
14
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useCallback } from 'react';
|
|
3
|
+
import { createNavStack, pushNav, popNav } from '../navigation.js';
|
|
4
|
+
const NavigationContext = createContext(null);
|
|
5
|
+
export function NavigationProvider({ initial, children, }) {
|
|
6
|
+
const [state, setState] = useState(() => createNavStack(initial));
|
|
7
|
+
const push = useCallback((entry) => setState((s) => pushNav(s, entry)), []);
|
|
8
|
+
const pop = useCallback(() => setState((s) => popNav(s)), []);
|
|
9
|
+
const value = {
|
|
10
|
+
current: state.current,
|
|
11
|
+
breadcrumbs: state.breadcrumbs,
|
|
12
|
+
canGoBack: state.canGoBack,
|
|
13
|
+
push,
|
|
14
|
+
pop,
|
|
15
|
+
};
|
|
16
|
+
return _jsx(NavigationContext.Provider, { value: value, children: children });
|
|
17
|
+
}
|
|
18
|
+
export function useNav() {
|
|
19
|
+
const ctx = useContext(NavigationContext);
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error('useNav must be used within a NavigationProvider');
|
|
22
|
+
}
|
|
23
|
+
return ctx;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=navigation.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface TerminalSize {
|
|
2
|
+
cols: number;
|
|
3
|
+
rows: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function TerminalSizeProvider({ children }: {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function useTerminalSize(): TerminalSize;
|
|
9
|
+
//# sourceMappingURL=terminal-size.d.ts.map
|