@kazurolabs/psql-explorer 0.0.0-stage → 0.3.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 +505 -2
- package/RELEASES.md +87 -0
- package/dist/app.js +39 -0
- package/dist/cli.js +307 -0
- package/dist/commands/configgen.js +128 -0
- package/dist/commands/explore.js +388 -0
- package/dist/commands/terminal-prompt.js +68 -0
- package/dist/config/load.js +324 -0
- package/dist/config/resolve-environment.js +112 -0
- package/dist/config/resolvers/env.js +12 -0
- package/dist/config/resolvers/file.js +24 -0
- package/dist/config/resolvers/groups.js +1 -0
- package/dist/config/resolvers/index.js +49 -0
- package/dist/config/resolvers/literal.js +7 -0
- package/dist/config/resolvers/prompt.js +159 -0
- package/dist/config/resolvers/stored.js +13 -0
- package/dist/config/resolvers/types.js +18 -0
- package/dist/config/resolvers/vault.js +283 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/source.js +105 -0
- package/dist/db/connect.js +55 -0
- package/dist/db/identifiers.js +6 -0
- package/dist/db/paths.js +14 -0
- package/dist/db/store.js +323 -0
- package/dist/db/tester.js +190 -0
- package/dist/db/validate.js +99 -0
- package/dist/environments.js +140 -0
- package/dist/explorer/alter.js +493 -0
- package/dist/explorer/catalog.js +414 -0
- package/dist/explorer/connect-entry.js +42 -0
- package/dist/explorer/cypher-parse.js +478 -0
- package/dist/explorer/cypher.js +963 -0
- package/dist/explorer/ddl.js +502 -0
- package/dist/explorer/dependents.js +222 -0
- package/dist/explorer/design-load.js +254 -0
- package/dist/explorer/design.js +45 -0
- package/dist/explorer/edit.js +108 -0
- package/dist/explorer/explain.js +37 -0
- package/dist/explorer/filter.js +121 -0
- package/dist/explorer/format.js +134 -0
- package/dist/explorer/graph.js +679 -0
- package/dist/explorer/gucs.js +31 -0
- package/dist/explorer/profile.js +102 -0
- package/dist/explorer/references.js +95 -0
- package/dist/explorer/rows.js +64 -0
- package/dist/explorer/schema-diff.js +360 -0
- package/dist/explorer/search.js +309 -0
- package/dist/explorer/security.js +152 -0
- package/dist/explorer/seed.js +1376 -0
- package/dist/explorer/session.js +122 -0
- package/dist/explorer/stats.js +135 -0
- package/dist/explorer/timeout.js +13 -0
- package/dist/prompts.js +52 -0
- package/dist/screens/environments.js +229 -0
- package/dist/screens/explorer.js +145 -0
- package/dist/screens/header.js +8 -0
- package/dist/screens/index.js +13 -0
- package/dist/screens/new-connection.js +178 -0
- package/dist/screens/panes/common.js +35 -0
- package/dist/screens/panes/history.js +36 -0
- package/dist/screens/panes/info.js +61 -0
- package/dist/screens/panes/query.js +113 -0
- package/dist/screens/panes/table.js +178 -0
- package/dist/screens/panes/tables.js +142 -0
- package/dist/screens/prompt-modal.js +18 -0
- package/dist/screens/record-view.js +42 -0
- package/dist/screens/result-grid.js +126 -0
- package/dist/screens/scroll.js +12 -0
- package/dist/screens/splash.js +78 -0
- package/dist/screens/terminal.js +25 -0
- package/dist/screens/types.js +1 -0
- package/dist/services.js +1 -0
- package/dist/startup.js +84 -0
- package/dist/web/server.js +988 -0
- package/package.json +69 -5
- package/web/dist/assets/anton-latin-400-normal-AUNGEG_V.woff +0 -0
- package/web/dist/assets/anton-latin-400-normal-Byf51wtH.woff2 +0 -0
- package/web/dist/assets/anton-latin-ext-400-normal-BMODBQc6.woff +0 -0
- package/web/dist/assets/anton-latin-ext-400-normal-SyiqE2Jt.woff2 +0 -0
- package/web/dist/assets/anton-vietnamese-400-normal-2FfR1wHA.woff +0 -0
- package/web/dist/assets/anton-vietnamese-400-normal-CkBxLiRJ.woff2 +0 -0
- package/web/dist/assets/antonio-latin-400-normal-B7iMKlzw.woff +0 -0
- package/web/dist/assets/antonio-latin-400-normal-CZjzwNAN.woff2 +0 -0
- package/web/dist/assets/antonio-latin-700-normal-BY3mCh47.woff +0 -0
- package/web/dist/assets/antonio-latin-700-normal-DSH-lEDG.woff2 +0 -0
- package/web/dist/assets/antonio-latin-ext-400-normal-nE82dizb.woff2 +0 -0
- package/web/dist/assets/antonio-latin-ext-400-normal-qA9Jb92z.woff +0 -0
- package/web/dist/assets/antonio-latin-ext-700-normal-BdSkr58W.woff +0 -0
- package/web/dist/assets/antonio-latin-ext-700-normal-D-dvgqjr.woff2 +0 -0
- package/web/dist/assets/button-ClA70PLR.js +1 -0
- package/web/dist/assets/code-xml-CXkpHeU9.js +1 -0
- package/web/dist/assets/compare-view-DKl5l_Jw.js +2 -0
- package/web/dist/assets/copy-CCXzeW-s.js +1 -0
- package/web/dist/assets/cypher-view-Pd0WFH8F.js +13 -0
- package/web/dist/assets/design-view-CIHSmH4u.js +9 -0
- package/web/dist/assets/erd-view-CV7mFGAN.js +1 -0
- package/web/dist/assets/format-sql-BLj5_RDZ.js +5 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/graph-force-yBLyErpS.js +1 -0
- package/web/dist/assets/graph-link-OCo_s0zO.js +1 -0
- package/web/dist/assets/graph-view-sM1OlByf.js +3 -0
- package/web/dist/assets/index-CDcnUacl.js +10 -0
- package/web/dist/assets/index-OzJqnJWG.css +2 -0
- package/web/dist/assets/key-round-Cv_7QeED.js +1 -0
- package/web/dist/assets/loader-circle-BK_GMdvN.js +1 -0
- package/web/dist/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
- package/web/dist/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
- package/web/dist/assets/orbitron-latin-900-normal-BbJatbhp.woff +0 -0
- package/web/dist/assets/orbitron-latin-900-normal-DrIi7unX.woff2 +0 -0
- package/web/dist/assets/query-builder-view-ChNFb51g.js +4 -0
- package/web/dist/assets/rotate-ccw-CdCaf7Q6.js +1 -0
- package/web/dist/assets/route-DO6IZ7of.js +15 -0
- package/web/dist/assets/seed-view-DFNB8WHE.js +7 -0
- package/web/dist/assets/share-tech-mono-latin-400-normal-15MgdvEM.woff +0 -0
- package/web/dist/assets/share-tech-mono-latin-400-normal-CCqdbj5z.woff2 +0 -0
- package/web/dist/assets/shield-alert-CMS-3j4U.js +1 -0
- package/web/dist/assets/sliders-horizontal-Dg51yc-C.js +1 -0
- package/web/dist/assets/sql-tabs-B4jt6bQj.js +2 -0
- package/web/dist/assets/table-DfMz2f8M.js +1 -0
- package/web/dist/assets/triangle-alert-COGnHCD1.js +1 -0
- package/web/dist/assets/zone-D9dXk8tn.js +5 -0
- package/web/dist/index.html +26 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { listColumns, listConstraints, listIndexes, tableLabel, } from '../../explorer/catalog.js';
|
|
5
|
+
import { formatBytes, formatDuration, formatEstimate } from '../../explorer/format.js';
|
|
6
|
+
import { browseRows, countRows, DEFAULT_PAGE_SIZE } from '../../explorer/rows.js';
|
|
7
|
+
import RecordView from '../record-view.js';
|
|
8
|
+
import ResultGrid, { startCursor } from '../result-grid.js';
|
|
9
|
+
import { gridHeight, Loading, PaneFrame, useLoader } from './common.js';
|
|
10
|
+
const views = ['rows', 'structure', 'indexes'];
|
|
11
|
+
const viewTitles = {
|
|
12
|
+
rows: 'Rows',
|
|
13
|
+
structure: 'Structure',
|
|
14
|
+
indexes: 'Indexes & constraints',
|
|
15
|
+
};
|
|
16
|
+
const text = (columns) => columns.map((name) => ({ name, type: 'text' }));
|
|
17
|
+
const structureColumns = text(['column', 'type', 'nullable', 'default', 'key', 'comment']);
|
|
18
|
+
const indexColumns = text(['name', 'kind', 'definition', 'size']);
|
|
19
|
+
const structureRows = (columns) => columns.map((column) => [
|
|
20
|
+
column.name,
|
|
21
|
+
column.type,
|
|
22
|
+
column.nullable ? 'yes' : 'no',
|
|
23
|
+
column.default,
|
|
24
|
+
[column.primaryKey ? 'PK' : null, column.references ? 'FK' : null]
|
|
25
|
+
.filter((tag) => tag !== null)
|
|
26
|
+
.join(' ') || null,
|
|
27
|
+
column.comment,
|
|
28
|
+
]);
|
|
29
|
+
const indexRows = (indexes, constraints) => [
|
|
30
|
+
...indexes.map((index) => [
|
|
31
|
+
index.name,
|
|
32
|
+
index.primary ? 'primary index' : index.unique ? 'unique index' : 'index',
|
|
33
|
+
index.definition,
|
|
34
|
+
formatBytes(index.bytes),
|
|
35
|
+
]),
|
|
36
|
+
...constraints.map((constraint) => [
|
|
37
|
+
constraint.name,
|
|
38
|
+
constraint.kind,
|
|
39
|
+
constraint.definition,
|
|
40
|
+
null,
|
|
41
|
+
]),
|
|
42
|
+
];
|
|
43
|
+
/** Sorted by the primary key when there is one, so pages do not overlap. */
|
|
44
|
+
const defaultOrder = (columns) => columns
|
|
45
|
+
.filter((column) => column.primaryKey)
|
|
46
|
+
.map((column) => ({ column: column.name, descending: false }));
|
|
47
|
+
/** One table: a page of its rows, its columns, and its indexes and constraints. */
|
|
48
|
+
export default function TablePane({ session, table, width, height, isInputActive, onClose }) {
|
|
49
|
+
const [view, setView] = useState('rows');
|
|
50
|
+
const [columns, loadColumns] = useLoader();
|
|
51
|
+
const [extras, loadExtras] = useLoader();
|
|
52
|
+
const [page, loadPage] = useLoader();
|
|
53
|
+
const [offset, setOffset] = useState(0);
|
|
54
|
+
// Undefined: the default order, once the columns are known.
|
|
55
|
+
const [order, setOrder] = useState();
|
|
56
|
+
const [count, loadCount] = useLoader();
|
|
57
|
+
const [cursors, setCursors] = useState({
|
|
58
|
+
rows: startCursor,
|
|
59
|
+
structure: startCursor,
|
|
60
|
+
indexes: startCursor,
|
|
61
|
+
});
|
|
62
|
+
const [record, setRecord] = useState();
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
loadColumns(() => listColumns(session, table));
|
|
65
|
+
loadExtras(async () => ({
|
|
66
|
+
indexes: await listIndexes(session, table),
|
|
67
|
+
constraints: await listConstraints(session, table),
|
|
68
|
+
}));
|
|
69
|
+
}, [loadColumns, loadExtras, session, table]);
|
|
70
|
+
const known = columns.state === 'ready' ? columns.value : undefined;
|
|
71
|
+
const sort = order ?? (known ? defaultOrder(known) : undefined);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!sort)
|
|
74
|
+
return;
|
|
75
|
+
loadPage(() => browseRows(session, table, { limit: DEFAULT_PAGE_SIZE, offset, orderBy: sort }));
|
|
76
|
+
// `sort` is a new array on every render; its content is what matters.
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [loadPage, session, table, offset, JSON.stringify(sort)]);
|
|
79
|
+
const setCursor = (update) => {
|
|
80
|
+
setCursors((previous) => ({ ...previous, [view]: update(previous[view]) }));
|
|
81
|
+
};
|
|
82
|
+
const rowsResult = page.state === 'ready' ? page.value : undefined;
|
|
83
|
+
const grid = view === 'rows'
|
|
84
|
+
? rowsResult
|
|
85
|
+
? { columns: rowsResult.columns, rows: rowsResult.rows }
|
|
86
|
+
: undefined
|
|
87
|
+
: view === 'structure'
|
|
88
|
+
? known
|
|
89
|
+
? { columns: structureColumns, rows: structureRows(known) }
|
|
90
|
+
: undefined
|
|
91
|
+
: extras.state === 'ready'
|
|
92
|
+
? { columns: indexColumns, rows: indexRows(extras.value.indexes, extras.value.constraints) }
|
|
93
|
+
: undefined;
|
|
94
|
+
const toggleSort = () => {
|
|
95
|
+
const column = rowsResult?.columns[cursors.rows.column]?.name;
|
|
96
|
+
if (!column)
|
|
97
|
+
return;
|
|
98
|
+
const current = sort?.length === 1 && sort[0]?.column === column ? sort[0] : undefined;
|
|
99
|
+
setOrder(!current
|
|
100
|
+
? [{ column, descending: false }]
|
|
101
|
+
: !current.descending
|
|
102
|
+
? [{ column, descending: true }]
|
|
103
|
+
: undefined);
|
|
104
|
+
setOffset(0);
|
|
105
|
+
};
|
|
106
|
+
useInput((input, key) => {
|
|
107
|
+
if (record !== undefined) {
|
|
108
|
+
if (key.escape || key.return)
|
|
109
|
+
setRecord(undefined);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (key.escape) {
|
|
113
|
+
onClose();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.tab) {
|
|
117
|
+
const at = views.indexOf(view);
|
|
118
|
+
setView(views[(at + (key.shift ? views.length - 1 : 1)) % views.length]);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (input === 'r') {
|
|
122
|
+
if (view === 'rows' && sort) {
|
|
123
|
+
loadPage(() => browseRows(session, table, { limit: DEFAULT_PAGE_SIZE, offset, orderBy: sort }));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
loadColumns(() => listColumns(session, table));
|
|
127
|
+
loadExtras(async () => ({
|
|
128
|
+
indexes: await listIndexes(session, table),
|
|
129
|
+
constraints: await listConstraints(session, table),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (view !== 'rows')
|
|
135
|
+
return;
|
|
136
|
+
if ((input === 'n' || input === ']') && rowsResult?.rows.length === DEFAULT_PAGE_SIZE) {
|
|
137
|
+
setOffset(offset + DEFAULT_PAGE_SIZE);
|
|
138
|
+
setCursor((previous) => ({ ...previous, row: 0, top: 0 }));
|
|
139
|
+
}
|
|
140
|
+
else if ((input === 'p' || input === '[') && offset > 0) {
|
|
141
|
+
setOffset(Math.max(0, offset - DEFAULT_PAGE_SIZE));
|
|
142
|
+
setCursor((previous) => ({ ...previous, row: 0, top: 0 }));
|
|
143
|
+
}
|
|
144
|
+
else if (input === 'o')
|
|
145
|
+
toggleSort();
|
|
146
|
+
else if (input === 'c')
|
|
147
|
+
loadCount(() => countRows(session, table));
|
|
148
|
+
}, { isActive: isInputActive });
|
|
149
|
+
const marks = new Map(view === 'rows'
|
|
150
|
+
? (sort ?? []).map(({ column, descending }) => [column, descending ? '▼' : '▲'])
|
|
151
|
+
: []);
|
|
152
|
+
const loading = view === 'rows' ? page : view === 'structure' ? columns : extras;
|
|
153
|
+
const tabs = (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: tableLabel(table) }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), views.map((candidate, index) => (_jsxs(Text, { children: [index > 0 ? _jsx(Text, { dimColor: true, children: " \u2502 " }) : null, _jsx(Text, { bold: candidate === view, color: candidate === view ? 'cyan' : 'gray', underline: candidate === view, children: viewTitles[candidate] })] }, candidate)))] }));
|
|
154
|
+
if (record !== undefined && rowsResult) {
|
|
155
|
+
return (_jsx(PaneFrame, { height: height, width: width, hint: "\u2191\u2193 fields \u00B7 \u2190\u2192 previous/next row \u00B7 Esc close", children: _jsx(RecordView, { columns: rowsResult.columns, row: rowsResult.rows[record] ?? [], title: `${tableLabel(table)} · row ${offset + record + 1}`, width: width, height: gridHeight(height), isInputActive: isInputActive, onStep: (by) => {
|
|
156
|
+
const next = record + by;
|
|
157
|
+
if (next >= 0 && next < rowsResult.rows.length)
|
|
158
|
+
setRecord(next);
|
|
159
|
+
} }, record) }));
|
|
160
|
+
}
|
|
161
|
+
const total = count.state === 'ready'
|
|
162
|
+
? `${count.value} rows`
|
|
163
|
+
: count.state === 'loading'
|
|
164
|
+
? 'counting…'
|
|
165
|
+
: count.state === 'failed'
|
|
166
|
+
? `count failed: ${count.message}`
|
|
167
|
+
: table.estimatedRows === null
|
|
168
|
+
? undefined
|
|
169
|
+
: `${formatEstimate(table.estimatedRows)} rows`;
|
|
170
|
+
const status = loading.state === 'failed' ? (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ["\u2717 ", loading.message] })) : view === 'rows' && rowsResult ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [rowsResult.rows.length === 0
|
|
171
|
+
? `no rows from ${offset + 1}`
|
|
172
|
+
: `rows ${offset + 1}–${offset + rowsResult.rows.length}`, total ? ` of ${total}` : '', sort && sort.length > 0
|
|
173
|
+
? ` · by ${sort.map((part) => part.column).join(', ')}`
|
|
174
|
+
: ' · unsorted', ` · ${formatDuration(rowsResult.durationMs)}`] })) : undefined;
|
|
175
|
+
return (_jsxs(PaneFrame, { height: height, width: width, status: status, hint: view === 'rows'
|
|
176
|
+
? '↑↓←→ move · Enter row · n/p page · o sort · c count · r reload · Tab view · Esc tables'
|
|
177
|
+
: '↑↓←→ move · r reload · Tab view · Esc tables', children: [tabs, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [loading.state === 'loading' || loading.state === 'idle' ? _jsx(Loading, {}) : null, grid && loading.state === 'ready' ? (_jsx(ResultGrid, { columns: grid.columns, rows: grid.rows, cursor: cursors[view], onCursor: setCursor, onOpen: view === 'rows' ? setRecord : undefined, width: width, height: gridHeight(height, 2), isInputActive: isInputActive && record === undefined, marks: marks, showTypes: view === 'rows', empty: view === 'rows' ? '(no rows)' : '(none)' })) : null] })] }));
|
|
178
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { listSchemas, listTables } from '../../explorer/catalog.js';
|
|
6
|
+
import { formatBytes, formatEstimate, pad, truncate } from '../../explorer/format.js';
|
|
7
|
+
import { clampTop, moveTo, startPosition } from '../scroll.js';
|
|
8
|
+
import { gridHeight, Loading, PaneFrame, useLoader } from './common.js';
|
|
9
|
+
import TablePane from './table.js';
|
|
10
|
+
const kindWidth = 13;
|
|
11
|
+
const rowsWidth = 9;
|
|
12
|
+
const sizeWidth = 10;
|
|
13
|
+
const maxNameWidth = 40;
|
|
14
|
+
const schemaSetting = (envKey) => `schema:${envKey}`;
|
|
15
|
+
/** The tables of one schema; Enter opens one. */
|
|
16
|
+
export default function TablesPane(props) {
|
|
17
|
+
const { session, services, entry, width, height, isInputActive, setNested } = props;
|
|
18
|
+
const [schema, setSchema] = useState(() => services.store.getSetting(schemaSetting(entry.key)) ?? session.params.schema);
|
|
19
|
+
const [schemas, loadSchemas] = useLoader();
|
|
20
|
+
const [tables, loadTables] = useLoader();
|
|
21
|
+
const [position, setPosition] = useState(startPosition);
|
|
22
|
+
const { selected } = position;
|
|
23
|
+
const [filter, setFilter] = useState('');
|
|
24
|
+
const [filtering, setFiltering] = useState(false);
|
|
25
|
+
const [picking, setPicking] = useState(false);
|
|
26
|
+
const [pickSelected, setPickSelected] = useState(0);
|
|
27
|
+
const [open, setOpen] = useState();
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
loadSchemas(() => listSchemas(session));
|
|
30
|
+
}, [loadSchemas, session]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
loadTables(() => listTables(session, schema));
|
|
33
|
+
}, [loadTables, session, schema]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setNested(filtering || picking || open !== undefined);
|
|
36
|
+
}, [filtering, picking, open, setNested]);
|
|
37
|
+
const all = tables.state === 'ready' ? tables.value : [];
|
|
38
|
+
const needle = filter.toLowerCase();
|
|
39
|
+
const shownTables = needle
|
|
40
|
+
? all.filter((table) => table.name.toLowerCase().includes(needle))
|
|
41
|
+
: all;
|
|
42
|
+
const schemaList = schemas.state === 'ready' ? schemas.value : [];
|
|
43
|
+
// Title line, column titles and the two "more" markers.
|
|
44
|
+
const visible = Math.max(1, gridHeight(height, 4 + (filtering || filter ? 1 : 0)));
|
|
45
|
+
const first = clampTop(position.top, selected, visible, shownTables.length);
|
|
46
|
+
const move = (to) => {
|
|
47
|
+
setPosition((previous) => moveTo(previous, to, visible, shownTables.length));
|
|
48
|
+
};
|
|
49
|
+
const chooseSchema = (name) => {
|
|
50
|
+
setPicking(false);
|
|
51
|
+
services.store.setSetting(schemaSetting(entry.key), name);
|
|
52
|
+
setFilter('');
|
|
53
|
+
setPosition(startPosition);
|
|
54
|
+
setSchema(name);
|
|
55
|
+
};
|
|
56
|
+
useInput((input, key) => {
|
|
57
|
+
if (picking) {
|
|
58
|
+
if (key.escape)
|
|
59
|
+
setPicking(false);
|
|
60
|
+
else if (key.return) {
|
|
61
|
+
const chosen = schemaList[pickSelected];
|
|
62
|
+
if (chosen)
|
|
63
|
+
chooseSchema(chosen.name);
|
|
64
|
+
}
|
|
65
|
+
else if (key.upArrow)
|
|
66
|
+
setPickSelected(Math.max(0, pickSelected - 1));
|
|
67
|
+
else if (key.downArrow)
|
|
68
|
+
setPickSelected(Math.min(schemaList.length - 1, pickSelected + 1));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (filtering) {
|
|
72
|
+
if (key.escape) {
|
|
73
|
+
setFilter('');
|
|
74
|
+
setFiltering(false);
|
|
75
|
+
}
|
|
76
|
+
else if (key.return) {
|
|
77
|
+
setFiltering(false);
|
|
78
|
+
const table = shownTables[selected];
|
|
79
|
+
if (table)
|
|
80
|
+
setOpen(table);
|
|
81
|
+
}
|
|
82
|
+
else if (key.upArrow)
|
|
83
|
+
move((at) => at - 1);
|
|
84
|
+
else if (key.downArrow)
|
|
85
|
+
move((at) => at + 1);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (input === '/') {
|
|
89
|
+
setFiltering(true);
|
|
90
|
+
move(() => 0);
|
|
91
|
+
}
|
|
92
|
+
else if (input === 's') {
|
|
93
|
+
setPickSelected(Math.max(0, schemaList.findIndex((candidate) => candidate.name === schema)));
|
|
94
|
+
setPicking(true);
|
|
95
|
+
}
|
|
96
|
+
else if (input === 'r') {
|
|
97
|
+
loadSchemas(() => listSchemas(session));
|
|
98
|
+
loadTables(() => listTables(session, schema));
|
|
99
|
+
}
|
|
100
|
+
else if (key.return) {
|
|
101
|
+
const table = shownTables[selected];
|
|
102
|
+
if (table)
|
|
103
|
+
setOpen(table);
|
|
104
|
+
}
|
|
105
|
+
else if (key.upArrow || input === 'k')
|
|
106
|
+
move((at) => at - 1);
|
|
107
|
+
else if (key.downArrow || input === 'j')
|
|
108
|
+
move((at) => at + 1);
|
|
109
|
+
else if (key.pageUp)
|
|
110
|
+
move((at) => at - visible);
|
|
111
|
+
else if (key.pageDown)
|
|
112
|
+
move((at) => at + visible);
|
|
113
|
+
else if (key.home || input === 'g')
|
|
114
|
+
move(() => 0);
|
|
115
|
+
else if (key.end || input === 'G')
|
|
116
|
+
move(() => shownTables.length - 1);
|
|
117
|
+
}, { isActive: isInputActive && open === undefined });
|
|
118
|
+
if (open) {
|
|
119
|
+
return (_jsx(TablePane, { ...props, table: open, onClose: () => {
|
|
120
|
+
setOpen(undefined);
|
|
121
|
+
} }));
|
|
122
|
+
}
|
|
123
|
+
if (picking) {
|
|
124
|
+
return (_jsxs(PaneFrame, { height: height, width: width, hint: "\u2191\u2193 move \u00B7 Enter choose \u00B7 Esc cancel", children: [_jsx(Text, { bold: true, children: "Choose a schema" }), _jsx(Text, { children: " " }), schemas.state === 'loading' ? _jsx(Loading, {}) : null, schemas.state === 'failed' ? _jsxs(Text, { color: "red", children: ["\u2717 ", schemas.message] }) : null, schemaList.map((candidate, index) => (_jsxs(Text, { color: index === pickSelected ? 'cyan' : undefined, children: [index === pickSelected ? '› ' : ' ', _jsx(Text, { bold: index === pickSelected, children: candidate.name }), _jsxs(Text, { dimColor: true, children: [' ', candidate.tables, " tables \u00B7 owner ", candidate.owner] })] }, candidate.name)))] }));
|
|
125
|
+
}
|
|
126
|
+
const nameWidth = Math.min(maxNameWidth, Math.max(8, ...shownTables.map((table) => table.name.length)));
|
|
127
|
+
const commentWidth = Math.max(0, width - 2 - nameWidth - kindWidth - rowsWidth - sizeWidth - 8);
|
|
128
|
+
const line = (name, kind, rows, size, comment) => `${pad(truncate(name, nameWidth), nameWidth)} ${pad(kind, kindWidth)} ${pad(rows, rowsWidth, true)} ${pad(size, sizeWidth, true)} ${truncate(comment, commentWidth)}`;
|
|
129
|
+
const page = shownTables.slice(first, first + visible);
|
|
130
|
+
const below = shownTables.length - first - page.length;
|
|
131
|
+
return (_jsxs(PaneFrame, { height: height, width: width, status: tables.state === 'failed' ? (_jsxs(Text, { color: "red", wrap: "truncate-end", children: ["\u2717 ", tables.message] })) : tables.state === 'ready' ? (_jsx(Text, { dimColor: true, children: shownTables.length === all.length
|
|
132
|
+
? `${all.length} in ${schema}`
|
|
133
|
+
: `${shownTables.length} of ${all.length} in ${schema} match "${filter}"` })) : undefined, hint: filtering
|
|
134
|
+
? 'Type to filter · ↑↓ move · Enter open · Esc clear'
|
|
135
|
+
: '↑↓ move · Enter open · / filter · s schema · r reload · 1-4 tabs · Esc back · q quit', children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Schema " }), _jsx(Text, { bold: true, color: "cyan", children: schema }), _jsx(Text, { dimColor: true, children: " (s to change)" })] }), filtering || filter ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "/ " }), _jsx(TextInput, { value: filter, onChange: (value) => {
|
|
136
|
+
setFilter(value);
|
|
137
|
+
setPosition(startPosition);
|
|
138
|
+
}, focus: isInputActive && filtering })] })) : null, tables.state === 'loading' ? _jsx(Loading, {}) : null, tables.state === 'ready' && all.length === 0 ? (_jsxs(Text, { dimColor: true, children: ["No tables in ", schema, ". Press s to choose another schema."] })) : null, tables.state === 'ready' && all.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, dimColor: true, wrap: "truncate-end", children: [' ', line('NAME', 'KIND', 'ROWS', 'SIZE', 'COMMENT')] }), _jsx(Text, { dimColor: true, children: first > 0 ? ` ↑ ${first} more` : ' ' }), page.map((table, offset) => {
|
|
139
|
+
const isSelected = first + offset === selected;
|
|
140
|
+
return (_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, wrap: "truncate-end", children: [isSelected ? '› ' : ' ', line(table.name, table.kind, formatEstimate(table.estimatedRows), formatBytes(table.totalBytes), table.comment ?? '')] }, table.name));
|
|
141
|
+
}), _jsx(Text, { dimColor: true, children: below > 0 ? ` ↓ ${below} more` : ' ' })] })) : null] }));
|
|
142
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 TextInput from 'ink-text-input';
|
|
5
|
+
// A question over whatever screen is showing. While it is open <App> turns the screen's keys off.
|
|
6
|
+
export default function PromptModal({ prompt, isInputActive }) {
|
|
7
|
+
const [value, setValue] = useState('');
|
|
8
|
+
useInput((_input, key) => {
|
|
9
|
+
if (key.escape)
|
|
10
|
+
prompt.cancel();
|
|
11
|
+
}, { isActive: isInputActive });
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { bold: true, children: prompt.challenge === undefined
|
|
13
|
+
? `${prompt.envName}.${prompt.field}`
|
|
14
|
+
: `${prompt.envName} is protected` }), prompt.challenge !== undefined && (_jsxs(Text, { children: ["Type", ' ', _jsx(Text, { bold: true, color: "yellow", children: prompt.challenge }), ' ', "to open it."] })), _jsxs(Box, { children: [_jsxs(Text, { children: [prompt.challenge !== undefined ? 'Words' : prompt.masked ? 'Secret' : 'Value', ":", ' '] }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: (answer) => {
|
|
15
|
+
if (answer)
|
|
16
|
+
prompt.submit(answer);
|
|
17
|
+
}, focus: isInputActive, mask: prompt.masked ? '*' : undefined })] }), _jsx(Text, { dimColor: true, children: "Enter submit \u00B7 Esc cancel" })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { nullText, truncate } from '../explorer/format.js';
|
|
5
|
+
import { clampTop, moveTo, startPosition } from './scroll.js';
|
|
6
|
+
const maxLabelWidth = 30;
|
|
7
|
+
// The title line and the two "more" markers.
|
|
8
|
+
const chromeLines = 3;
|
|
9
|
+
/** One row, a field per line, with every value in full (long ones wrap). The owner handles Esc. */
|
|
10
|
+
export default function RecordView({ columns, row, title, width, height, isInputActive, onStep, }) {
|
|
11
|
+
const [position, setPosition] = useState(startPosition);
|
|
12
|
+
const { selected } = position;
|
|
13
|
+
const visible = Math.max(1, height - chromeLines);
|
|
14
|
+
const first = clampTop(position.top, selected, visible, columns.length);
|
|
15
|
+
const labelWidth = Math.min(maxLabelWidth, Math.max(...columns.map((column) => column.name.length)) + 2);
|
|
16
|
+
useInput((input, key) => {
|
|
17
|
+
if (key.leftArrow)
|
|
18
|
+
onStep?.(-1);
|
|
19
|
+
else if (key.rightArrow)
|
|
20
|
+
onStep?.(1);
|
|
21
|
+
const move = key.upArrow
|
|
22
|
+
? -1
|
|
23
|
+
: key.downArrow
|
|
24
|
+
? 1
|
|
25
|
+
: key.pageUp
|
|
26
|
+
? -visible
|
|
27
|
+
: key.pageDown
|
|
28
|
+
? visible
|
|
29
|
+
: 0;
|
|
30
|
+
if (move === 0)
|
|
31
|
+
return;
|
|
32
|
+
setPosition((previous) => moveTo(previous, (at) => at + move, visible, columns.length));
|
|
33
|
+
}, { isActive: isInputActive });
|
|
34
|
+
const shown = columns.slice(first, first + visible);
|
|
35
|
+
const below = columns.length - first - shown.length;
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, overflow: "hidden", children: [_jsx(Text, { bold: true, children: title }), _jsx(Text, { dimColor: true, children: first > 0 ? `↑ ${first} more` : ' ' }), shown.map((column, offset) => {
|
|
37
|
+
const index = first + offset;
|
|
38
|
+
const value = row[index] ?? null;
|
|
39
|
+
const isSelected = index === selected;
|
|
40
|
+
return (_jsxs(Box, { width: width, children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : undefined, children: truncate(column.name, labelWidth - 2) }) }), _jsxs(Box, { flexGrow: 1, flexShrink: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: value === null, wrap: "wrap", children: value ?? nullText }), isSelected ? _jsx(Text, { dimColor: true, children: column.type }) : null] })] }, `${index}-${column.name}`));
|
|
41
|
+
}), _jsx(Text, { dimColor: true, children: below > 0 ? `↓ ${below} more` : ' ' })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { cellText, isNumeric, pad, truncate, widthOf } from '../explorer/format.js';
|
|
4
|
+
import { clampTop } from './scroll.js';
|
|
5
|
+
export const startCursor = { row: 0, column: 0, top: 0, left: 0 };
|
|
6
|
+
const maxColumnWidth = 40;
|
|
7
|
+
const minColumnWidth = 4;
|
|
8
|
+
const gap = 2;
|
|
9
|
+
// Header lines (name, type) and the two "more" markers.
|
|
10
|
+
const chromeLines = 4;
|
|
11
|
+
/** How wide each column is drawn: as wide as its widest value, within bounds. */
|
|
12
|
+
export function columnWidths(columns, rows, marks, showTypes = true) {
|
|
13
|
+
return columns.map((column, index) => {
|
|
14
|
+
const mark = marks?.get(column.name);
|
|
15
|
+
let width = widthOf(column.name) + (mark ? widthOf(mark) + 1 : 0);
|
|
16
|
+
if (showTypes)
|
|
17
|
+
width = Math.max(width, widthOf(column.type));
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
width = Math.max(width, widthOf(cellText(row[index] ?? null)));
|
|
20
|
+
if (width >= maxColumnWidth)
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
return Math.max(minColumnWidth, Math.min(maxColumnWidth, width));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** The columns in view from `left` on: as many as fit in `width`, and at least one. */
|
|
27
|
+
export function visibleColumns(widths, left, width) {
|
|
28
|
+
let used = 0;
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (let index = left; index < widths.length; index++) {
|
|
31
|
+
const next = used + (count > 0 ? gap : 0) + (widths[index] ?? 0);
|
|
32
|
+
if (next > width && count > 0)
|
|
33
|
+
break;
|
|
34
|
+
used = next;
|
|
35
|
+
count++;
|
|
36
|
+
}
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
/** The first column in view that keeps `column` on screen, moving as little as possible. */
|
|
40
|
+
export function clampLeft(widths, left, column, width) {
|
|
41
|
+
let next = Math.min(left, column);
|
|
42
|
+
while (next < column && next + visibleColumns(widths, next, width) <= column)
|
|
43
|
+
next++;
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
/** A table of rows to move around in cell by cell, like a spreadsheet. */
|
|
47
|
+
export default function ResultGrid({ columns, rows, cursor, onCursor, onOpen, width, height, isInputActive, marks, empty = '(no rows)', showTypes = true, }) {
|
|
48
|
+
const widths = columnWidths(columns, rows, marks, showTypes);
|
|
49
|
+
const visible = Math.max(1, height - chromeLines + (showTypes ? 0 : 1));
|
|
50
|
+
const row = Math.min(cursor.row, Math.max(0, rows.length - 1));
|
|
51
|
+
const column = Math.min(cursor.column, Math.max(0, columns.length - 1));
|
|
52
|
+
const top = clampTop(cursor.top, row, visible, rows.length);
|
|
53
|
+
const left = clampLeft(widths, cursor.left, column, width);
|
|
54
|
+
useInput((input, key) => {
|
|
55
|
+
if (rows.length === 0 && columns.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
if (key.return) {
|
|
58
|
+
if (rows.length > 0)
|
|
59
|
+
onOpen?.(row);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const step = (previous) => {
|
|
63
|
+
const { row: at, column: on } = previous;
|
|
64
|
+
if (key.upArrow || input === 'k')
|
|
65
|
+
return [at - 1, on];
|
|
66
|
+
if (key.downArrow || input === 'j')
|
|
67
|
+
return [at + 1, on];
|
|
68
|
+
if (key.leftArrow || input === 'h')
|
|
69
|
+
return [at, on - 1];
|
|
70
|
+
if (key.rightArrow || input === 'l')
|
|
71
|
+
return [at, on + 1];
|
|
72
|
+
if (key.pageUp)
|
|
73
|
+
return [at - visible, on];
|
|
74
|
+
if (key.pageDown)
|
|
75
|
+
return [at + visible, on];
|
|
76
|
+
if (key.home || input === 'g')
|
|
77
|
+
return [0, on];
|
|
78
|
+
if (key.end || input === 'G')
|
|
79
|
+
return [rows.length - 1, on];
|
|
80
|
+
if (input === '^' || input === '0')
|
|
81
|
+
return [at, 0];
|
|
82
|
+
if (input === '$')
|
|
83
|
+
return [at, columns.length - 1];
|
|
84
|
+
return undefined;
|
|
85
|
+
};
|
|
86
|
+
if (!step({ row, column }))
|
|
87
|
+
return;
|
|
88
|
+
onCursor((previous) => {
|
|
89
|
+
const [nextRow, nextColumn] = step({
|
|
90
|
+
row: Math.min(previous.row, Math.max(0, rows.length - 1)),
|
|
91
|
+
column: Math.min(previous.column, Math.max(0, columns.length - 1)),
|
|
92
|
+
});
|
|
93
|
+
const clampedRow = Math.max(0, Math.min(rows.length - 1, nextRow));
|
|
94
|
+
const clampedColumn = Math.max(0, Math.min(columns.length - 1, nextColumn));
|
|
95
|
+
return {
|
|
96
|
+
row: clampedRow,
|
|
97
|
+
column: clampedColumn,
|
|
98
|
+
top: clampTop(previous.top, clampedRow, visible, rows.length),
|
|
99
|
+
left: clampLeft(widths, previous.left, clampedColumn, width),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}, { isActive: isInputActive });
|
|
103
|
+
if (columns.length === 0)
|
|
104
|
+
return _jsx(Text, { dimColor: true, children: empty });
|
|
105
|
+
const count = visibleColumns(widths, left, width);
|
|
106
|
+
const shown = Array.from({ length: count }, (_, offset) => left + offset);
|
|
107
|
+
const hiddenRight = columns.length - left - count;
|
|
108
|
+
const header = (text) => shown
|
|
109
|
+
.map((index) => pad(truncate(text(index), widths[index]), widths[index]))
|
|
110
|
+
.join(' '.repeat(gap));
|
|
111
|
+
const page = rows.slice(top, top + visible);
|
|
112
|
+
const below = rows.length - top - page.length;
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: header((index) => {
|
|
114
|
+
const mark = marks?.get(columns[index].name);
|
|
115
|
+
return mark ? `${columns[index].name} ${mark}` : columns[index].name;
|
|
116
|
+
}) }), showTypes ? (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: header((index) => columns[index].type) })) : null, _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [top > 0 ? `↑ ${top} more` : ' ', left > 0 ? ` ← ${left} columns` : '', hiddenRight > 0 ? ` → ${hiddenRight} columns` : ''] }), rows.length === 0 ? _jsx(Text, { dimColor: true, children: empty }) : null, page.map((values, offset) => {
|
|
117
|
+
const index = top + offset;
|
|
118
|
+
const isSelected = index === row;
|
|
119
|
+
return (_jsx(Text, { wrap: "truncate-end", children: shown.map((columnIndex, position) => {
|
|
120
|
+
const value = values[columnIndex] ?? null;
|
|
121
|
+
const columnWidth = widths[columnIndex];
|
|
122
|
+
const text = pad(truncate(cellText(value), columnWidth), columnWidth, isNumeric(columns[columnIndex]));
|
|
123
|
+
return (_jsxs(Text, { children: [position > 0 ? ' '.repeat(gap) : '', _jsx(Text, { inverse: isSelected && columnIndex === column, color: isSelected ? 'cyan' : undefined, dimColor: value === null, children: text })] }, columnIndex));
|
|
124
|
+
}) }, index));
|
|
125
|
+
}), _jsx(Text, { dimColor: true, children: below > 0 ? `↓ ${below} more` : ' ' })] }));
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// First visible row of a scrolling list: keeps `selected` in view and the window filled.
|
|
2
|
+
export const clampTop = (top, selected, visible, total) => Math.max(0, Math.min(top, selected, total - visible), selected - visible + 1);
|
|
3
|
+
export const startPosition = { selected: 0, top: 0 };
|
|
4
|
+
/**
|
|
5
|
+
* The position after moving to `to(selected)` in a list of `total` rows, `visible` at a time. Given
|
|
6
|
+
* to a state updater, so each of several keys that arrive together (key repeat sends them in one
|
|
7
|
+
* chunk) moves on from where the one before left off.
|
|
8
|
+
*/
|
|
9
|
+
export const moveTo = (position, to, visible, total) => {
|
|
10
|
+
const selected = Math.max(0, Math.min(total - 1, to(position.selected)));
|
|
11
|
+
return { selected, top: clampTop(position.top, selected, visible, total) };
|
|
12
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import BigText from 'ink-big-text';
|
|
6
|
+
import Gradient from 'ink-gradient';
|
|
7
|
+
import { useTerminalSize } from './terminal.js';
|
|
8
|
+
export const tagline = 'Explore your Postgres databases from the terminal';
|
|
9
|
+
// Rendered widths of "Explorer": cfonts "block" is about 73 columns, "simple" 58, "tiny" 32. Each
|
|
10
|
+
// threshold leaves a little margin so the art never touches the edge or wraps into broken rows.
|
|
11
|
+
const blockMinColumns = 80;
|
|
12
|
+
const simpleMinColumns = 64;
|
|
13
|
+
const tinyMinColumns = 40;
|
|
14
|
+
function Title({ columns }) {
|
|
15
|
+
if (columns >= blockMinColumns)
|
|
16
|
+
return _jsx(BigText, { text: "Explorer", font: "block" });
|
|
17
|
+
if (columns >= simpleMinColumns)
|
|
18
|
+
return _jsx(BigText, { text: "Explorer", font: "simple" });
|
|
19
|
+
if (columns >= tinyMinColumns)
|
|
20
|
+
return _jsx(BigText, { text: "Explorer", font: "tiny" });
|
|
21
|
+
// Too narrow for any ASCII art: a plain title still reads cleanly.
|
|
22
|
+
return _jsx(Text, { bold: true, children: "psql-explorer" });
|
|
23
|
+
}
|
|
24
|
+
const company = 'Kazuro Labs';
|
|
25
|
+
// A line with a diamond in the middle, as wide as the terminal allows (at most `maxRuleWidth`).
|
|
26
|
+
const maxRuleWidth = 56;
|
|
27
|
+
const rule = (columns) => {
|
|
28
|
+
const width = Math.max(9, Math.min(maxRuleWidth, columns - 4));
|
|
29
|
+
const side = '━'.repeat(Math.floor((width - 3) / 2));
|
|
30
|
+
return `${side} ◆ ${side}`;
|
|
31
|
+
};
|
|
32
|
+
const highlights = ['tables & rows', 'SQL from the keyboard', 'Vault-ready'];
|
|
33
|
+
const highlightsMinColumns = 64;
|
|
34
|
+
const noSubscribe = () => () => undefined;
|
|
35
|
+
const noSteps = [];
|
|
36
|
+
const noSnapshot = () => noSteps;
|
|
37
|
+
const maxStepsWidth = 90;
|
|
38
|
+
// A step that has taken a while says for how long, so a slow Vault does not look stuck.
|
|
39
|
+
const showElapsedAfterMs = 3000;
|
|
40
|
+
// Starts the startup steps on mount and follows them. Without any, everything was ready already.
|
|
41
|
+
function useStartup(startup) {
|
|
42
|
+
const steps = useSyncExternalStore(startup ? startup.subscribe : noSubscribe, startup ? startup.snapshot : noSnapshot);
|
|
43
|
+
const [now, setNow] = useState(() => Date.now());
|
|
44
|
+
const finished = startup ? startup.finished() : true;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
void startup?.start();
|
|
47
|
+
}, [startup]);
|
|
48
|
+
// Re-render every second while something runs, for the elapsed time.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (finished)
|
|
51
|
+
return;
|
|
52
|
+
const timer = setInterval(() => {
|
|
53
|
+
setNow(Date.now());
|
|
54
|
+
}, 1000);
|
|
55
|
+
return () => {
|
|
56
|
+
clearInterval(timer);
|
|
57
|
+
};
|
|
58
|
+
}, [finished]);
|
|
59
|
+
return { steps, finished, now };
|
|
60
|
+
}
|
|
61
|
+
function StepLine({ step, now }) {
|
|
62
|
+
const seconds = Math.max(0, Math.floor((now - step.startedAt) / 1000));
|
|
63
|
+
const elapsed = step.state === 'running' && seconds * 1000 >= showElapsedAfterMs;
|
|
64
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [step.state === 'running' ? (_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) })) : step.state === 'done' ? (_jsx(Text, { color: "green", children: "\u2713" })) : step.state === 'warn' ? (_jsx(Text, { color: "yellow", children: "!" })) : (_jsx(Text, { color: "red", children: "\u2717" })), ' ', step.label, step.detail ? (_jsxs(Text, { color: step.state === 'failed' ? 'red' : step.state === 'warn' ? 'yellow' : undefined, dimColor: step.state === 'running' || step.state === 'done', children: [' ', "\u00B7 ", step.detail] })) : null, elapsed ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", seconds, "s"] }) : null] }));
|
|
65
|
+
}
|
|
66
|
+
export default function Splash({ version, navigate, services, isInputActive, }) {
|
|
67
|
+
const { columns, rows } = useTerminalSize();
|
|
68
|
+
const { steps, finished, now } = useStartup(services.startup);
|
|
69
|
+
const stepsWidth = Math.max(20, Math.min(columns - 4, maxStepsWidth));
|
|
70
|
+
useInput((input) => {
|
|
71
|
+
// `q` is the global quit key, handled by <App>.
|
|
72
|
+
if (input === 'o' && services.webUrl)
|
|
73
|
+
services.openUrl?.(services.webUrl);
|
|
74
|
+
else if (input !== 'q' && finished)
|
|
75
|
+
navigate('environments');
|
|
76
|
+
}, { isActive: isInputActive });
|
|
77
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: Math.max(1, rows - 1), paddingY: 1, children: [_jsx(Box, { "aria-label": "psql-explorer", marginBottom: columns >= tinyMinColumns ? 0 : 1, children: _jsx(Gradient, { name: "cristal", children: _jsx(Title, { columns: columns }) }) }), _jsx(Gradient, { name: "cristal", children: _jsx(Text, { children: rule(columns) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: tagline }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "by " }), _jsx(Gradient, { name: "vice", children: _jsx(Text, { bold: true, children: company }) })] }), columns >= highlightsMinColumns ? (_jsx(Box, { marginTop: 1, children: highlights.map((item, index) => (_jsxs(Text, { color: "gray", children: [index > 0 ? ' ' : '', _jsx(Text, { color: "cyan", children: "\u25C6" }), " ", item] }, item))) })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["v", version] }) }), services.webUrl ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Web UI " }), _jsx(Text, { color: "cyan", children: services.webUrl }), _jsx(Text, { dimColor: true, children: " \u00B7 press " }), _jsx(Text, { color: "cyan", children: "o" }), _jsx(Text, { dimColor: true, children: " to open" })] })) : null, steps.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, width: stepsWidth, children: steps.map((step) => (_jsx(StepLine, { step: step, now: now }, step.id))) })) : null, _jsx(Box, { marginTop: 1, children: !finished ? (_jsx(Text, { color: "gray", children: "Starting\u2026 \u00B7 q to quit" })) : columns >= tinyMinColumns ? (_jsx(Text, { color: "gray", children: "Press any key to continue \u00B7 q to quit" })) : (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: "gray", children: "Press any key to continue" }), _jsx(Text, { color: "gray", children: "q to quit" })] })) })] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useStdout } from 'ink';
|
|
3
|
+
// A standard terminal, for when stdout does not report its size (e.g. piped output).
|
|
4
|
+
const fallback = { columns: 80, rows: 24 };
|
|
5
|
+
// Ink types stdout as a plain WritableStream; a TTY stream also carries `columns` and `rows`.
|
|
6
|
+
const sizeOf = (stdout) => {
|
|
7
|
+
const tty = stdout;
|
|
8
|
+
return { columns: tty.columns || fallback.columns, rows: tty.rows || fallback.rows };
|
|
9
|
+
};
|
|
10
|
+
// The size of the stream Ink renders to, updated on 'resize'. (Ink's useWindowSize falls back to
|
|
11
|
+
// the real terminal when the stream reports no rows, which breaks rendering into a test stream.)
|
|
12
|
+
export function useTerminalSize() {
|
|
13
|
+
const { stdout } = useStdout();
|
|
14
|
+
const [size, setSize] = useState(() => sizeOf(stdout));
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const onResize = () => {
|
|
17
|
+
setSize(sizeOf(stdout));
|
|
18
|
+
};
|
|
19
|
+
stdout.on('resize', onResize);
|
|
20
|
+
return () => {
|
|
21
|
+
stdout.off('resize', onResize);
|
|
22
|
+
};
|
|
23
|
+
}, [stdout]);
|
|
24
|
+
return size;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/services.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|