dockza.app 0.2.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 +22 -0
- package/NOTICE +69 -0
- package/README.md +203 -0
- package/dist/cli.js +33 -0
- package/dist/docker/client.js +76 -0
- package/dist/docker/containers.js +180 -0
- package/dist/docker/images.js +41 -0
- package/dist/docker/networks.js +66 -0
- package/dist/docker/volumes.js +44 -0
- package/dist/models/docker.js +2 -0
- package/dist/theme.js +55 -0
- package/dist/ui/app.js +517 -0
- package/dist/ui/containers/confirm-dialog.js +84 -0
- package/dist/ui/containers/container-detail.js +189 -0
- package/dist/ui/containers/container-list.js +120 -0
- package/dist/ui/containers/containers-tab.js +314 -0
- package/dist/ui/containers/log-viewer.js +190 -0
- package/dist/ui/footer.js +193 -0
- package/dist/ui/help-overlay.js +122 -0
- package/dist/ui/images/images-tab.js +37 -0
- package/dist/ui/networks/networks-tab.js +45 -0
- package/dist/ui/resource-list-tab.js +183 -0
- package/dist/ui/side-rail.js +132 -0
- package/dist/ui/stacks/stack-tree.js +346 -0
- package/dist/ui/stacks/stacks-tab.js +392 -0
- package/dist/ui/top-bar.js +78 -0
- package/dist/ui/volumes/volumes-tab.js +40 -0
- package/dist/ui/widgets.js +61 -0
- package/dist/utils/external-terminal.js +84 -0
- package/dist/utils/format.js +65 -0
- package/dist/utils/stacks.js +76 -0
- package/dist/utils/stats.js +12 -0
- package/dist/utils/status.js +80 -0
- package/dist/utils/term-caps.js +14 -0
- package/package.json +77 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StackTree = void 0;
|
|
7
|
+
exports.computeColumns = computeColumns;
|
|
8
|
+
const neo_blessed_1 = __importDefault(require("neo-blessed"));
|
|
9
|
+
const _theme_1 = require("../../theme");
|
|
10
|
+
const format_1 = require("../../utils/format");
|
|
11
|
+
const status_1 = require("../../utils/status");
|
|
12
|
+
const widgets_1 = require("../widgets");
|
|
13
|
+
function computeColumns(width) {
|
|
14
|
+
const indentW = 3;
|
|
15
|
+
const statusW = 16;
|
|
16
|
+
const cpuW = 8;
|
|
17
|
+
const memW = 8;
|
|
18
|
+
const portsW = 22;
|
|
19
|
+
const showPorts = width >= 90;
|
|
20
|
+
const showMem = width >= 70;
|
|
21
|
+
const showCpu = width >= 55;
|
|
22
|
+
const fixedAfterIndent = statusW + (showCpu ? cpuW : 0) + (showMem ? memW : 0) + (showPorts ? portsW : 0);
|
|
23
|
+
const flex = Math.max(18, width - indentW - fixedAfterIndent);
|
|
24
|
+
const nameW = Math.max(10, Math.floor(flex * 0.55));
|
|
25
|
+
const imageW = Math.max(8, flex - nameW);
|
|
26
|
+
return {
|
|
27
|
+
totalWidth: width,
|
|
28
|
+
indentW,
|
|
29
|
+
nameW,
|
|
30
|
+
imageW,
|
|
31
|
+
statusW,
|
|
32
|
+
cpuW,
|
|
33
|
+
memW,
|
|
34
|
+
portsW: showPorts ? portsW : 0,
|
|
35
|
+
showCpu,
|
|
36
|
+
showMem,
|
|
37
|
+
showPorts,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
class StackTree {
|
|
41
|
+
wrapper;
|
|
42
|
+
header;
|
|
43
|
+
list;
|
|
44
|
+
messageBox;
|
|
45
|
+
stacks = [];
|
|
46
|
+
statsCache = new Map();
|
|
47
|
+
rows = [];
|
|
48
|
+
expanded = new Set();
|
|
49
|
+
seenStacks = new Set();
|
|
50
|
+
filter = '';
|
|
51
|
+
updating = false;
|
|
52
|
+
navigateHandlers = [];
|
|
53
|
+
constructor(parent, dims) {
|
|
54
|
+
this.wrapper = neo_blessed_1.default.box({
|
|
55
|
+
parent,
|
|
56
|
+
top: dims.top,
|
|
57
|
+
left: dims.left,
|
|
58
|
+
width: dims.width,
|
|
59
|
+
height: dims.height,
|
|
60
|
+
});
|
|
61
|
+
this.header = (0, widgets_1.createHeaderBar)(this.wrapper);
|
|
62
|
+
this.list = (0, widgets_1.createListWidget)(this.wrapper);
|
|
63
|
+
this.messageBox = (0, widgets_1.createCenteredMessage)(this.wrapper);
|
|
64
|
+
this.list.on('select item', (_item, index) => {
|
|
65
|
+
if (this.updating)
|
|
66
|
+
return;
|
|
67
|
+
this.emitNavigate(index);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
on(event, handler) {
|
|
71
|
+
if (event === 'navigate')
|
|
72
|
+
this.navigateHandlers.push(handler);
|
|
73
|
+
}
|
|
74
|
+
show() {
|
|
75
|
+
this.wrapper.show();
|
|
76
|
+
}
|
|
77
|
+
hide() {
|
|
78
|
+
this.wrapper.hide();
|
|
79
|
+
}
|
|
80
|
+
focus() {
|
|
81
|
+
this.list.focus();
|
|
82
|
+
}
|
|
83
|
+
showLoading() {
|
|
84
|
+
this.messageBox.setContent(_theme_1.t.dim(' Loading…'));
|
|
85
|
+
this.messageBox.show();
|
|
86
|
+
}
|
|
87
|
+
setData(stacks, statsCache) {
|
|
88
|
+
// Auto-expand live stacks the first time we see them (e.g. on startup).
|
|
89
|
+
for (const s of stacks) {
|
|
90
|
+
if (!this.seenStacks.has(s.id)) {
|
|
91
|
+
this.seenStacks.add(s.id);
|
|
92
|
+
if (s.isLive)
|
|
93
|
+
this.expanded.add(s.id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.stacks = stacks;
|
|
97
|
+
this.statsCache = statsCache;
|
|
98
|
+
this.rebuild({ preserveSelection: true });
|
|
99
|
+
}
|
|
100
|
+
setFilter(q) {
|
|
101
|
+
this.filter = q;
|
|
102
|
+
this.rebuild({ preserveSelection: false });
|
|
103
|
+
}
|
|
104
|
+
getFilter() {
|
|
105
|
+
return this.filter;
|
|
106
|
+
}
|
|
107
|
+
toggleExpansion() {
|
|
108
|
+
const sel = this.rows[(0, widgets_1.listSelected)(this.list)];
|
|
109
|
+
if (!sel || sel.kind !== 'stack-header')
|
|
110
|
+
return false;
|
|
111
|
+
if (this.expanded.has(sel.stackId))
|
|
112
|
+
this.expanded.delete(sel.stackId);
|
|
113
|
+
else
|
|
114
|
+
this.expanded.add(sel.stackId);
|
|
115
|
+
this.rebuild({ preserveSelection: true });
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
expandSelected() {
|
|
119
|
+
const sel = this.rows[(0, widgets_1.listSelected)(this.list)];
|
|
120
|
+
if (!sel)
|
|
121
|
+
return;
|
|
122
|
+
if (sel.kind === 'stack-header' && !this.expanded.has(sel.stackId)) {
|
|
123
|
+
this.expanded.add(sel.stackId);
|
|
124
|
+
this.rebuild({ preserveSelection: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
collapseSelected() {
|
|
128
|
+
const idx = (0, widgets_1.listSelected)(this.list);
|
|
129
|
+
const sel = this.rows[idx];
|
|
130
|
+
if (!sel)
|
|
131
|
+
return;
|
|
132
|
+
if (sel.kind === 'stack-header' && this.expanded.has(sel.stackId)) {
|
|
133
|
+
this.expanded.delete(sel.stackId);
|
|
134
|
+
this.rebuild({ preserveSelection: true });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (sel.kind === 'service') {
|
|
138
|
+
const headerIdx = this.rows.findIndex((r) => r.kind === 'stack-header' && r.stackId === sel.stackId);
|
|
139
|
+
if (headerIdx >= 0) {
|
|
140
|
+
this.updating = true;
|
|
141
|
+
this.list.select(headerIdx);
|
|
142
|
+
this.updating = false;
|
|
143
|
+
this.emitNavigate(headerIdx);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
jumpToStack(stackId) {
|
|
148
|
+
this.expanded.add(stackId);
|
|
149
|
+
this.rebuild({ preserveSelection: false });
|
|
150
|
+
const idx = this.rows.findIndex((r) => r.kind === 'stack-header' && r.stackId === stackId);
|
|
151
|
+
if (idx >= 0) {
|
|
152
|
+
this.updating = true;
|
|
153
|
+
this.list.select(idx);
|
|
154
|
+
this.updating = false;
|
|
155
|
+
this.emitNavigate(idx);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
getSelected() {
|
|
159
|
+
const idx = (0, widgets_1.listSelected)(this.list);
|
|
160
|
+
return this.rowToSelection(this.rows[idx]);
|
|
161
|
+
}
|
|
162
|
+
/** Re-render the current dataset (call after terminal resize). */
|
|
163
|
+
redraw() {
|
|
164
|
+
this.rebuild({ preserveSelection: true });
|
|
165
|
+
}
|
|
166
|
+
rebuild({ preserveSelection }) {
|
|
167
|
+
const prevKey = preserveSelection ? this.currentSelectionKey() : null;
|
|
168
|
+
this.messageBox.hide();
|
|
169
|
+
this.rows = this.flatten();
|
|
170
|
+
const width = Math.max(20, Number(this.list.width) - 2);
|
|
171
|
+
const cols = computeColumns(width);
|
|
172
|
+
this.renderHeader(cols);
|
|
173
|
+
const items = this.rows.map((r) => this.renderRow(r, cols));
|
|
174
|
+
this.updating = true;
|
|
175
|
+
this.list.setItems(items);
|
|
176
|
+
let target = 0;
|
|
177
|
+
if (prevKey) {
|
|
178
|
+
const found = this.rows.findIndex((r) => this.rowKey(r) === prevKey);
|
|
179
|
+
if (found >= 0)
|
|
180
|
+
target = found;
|
|
181
|
+
}
|
|
182
|
+
target = Math.min(target, Math.max(0, this.rows.length - 1));
|
|
183
|
+
this.list.select(target);
|
|
184
|
+
this.updating = false;
|
|
185
|
+
if (this.rows.length === 0) {
|
|
186
|
+
const msg = this.filter ? ` No matches for ${_theme_1.t.fg(this.filter)}` : ' No stacks';
|
|
187
|
+
this.messageBox.setContent(_theme_1.t.dim(msg));
|
|
188
|
+
this.messageBox.show();
|
|
189
|
+
}
|
|
190
|
+
this.emitNavigate(target);
|
|
191
|
+
}
|
|
192
|
+
flatten() {
|
|
193
|
+
const out = [];
|
|
194
|
+
const needle = this.filter.toLowerCase();
|
|
195
|
+
for (const s of this.stacks) {
|
|
196
|
+
let services = s.services;
|
|
197
|
+
const stackMatches = !needle || s.id.toLowerCase().includes(needle);
|
|
198
|
+
if (needle && !stackMatches) {
|
|
199
|
+
services = s.services.filter((c) => c.name.toLowerCase().includes(needle) || c.image.toLowerCase().includes(needle));
|
|
200
|
+
if (services.length === 0)
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const expanded = this.expanded.has(s.id) || (Boolean(needle) && !stackMatches);
|
|
204
|
+
out.push({
|
|
205
|
+
kind: 'stack-header',
|
|
206
|
+
stackId: s.id,
|
|
207
|
+
stack: s,
|
|
208
|
+
expanded,
|
|
209
|
+
});
|
|
210
|
+
if (expanded) {
|
|
211
|
+
for (const c of services) {
|
|
212
|
+
out.push({ kind: 'service', stackId: s.id, container: c });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
renderHeader(cols) {
|
|
219
|
+
const indent = (0, format_1.padEnd)('', cols.indentW);
|
|
220
|
+
const name = (0, format_1.padEnd)(_theme_1.t.dim('STACK · SERVICE'), cols.nameW + cols.imageW + 1);
|
|
221
|
+
const status = (0, format_1.padEnd)(_theme_1.t.dim('STATE'), cols.statusW);
|
|
222
|
+
const cpu = cols.showCpu ? (0, format_1.padEnd)(_theme_1.t.dim('CPU'), cols.cpuW) : '';
|
|
223
|
+
const mem = cols.showMem ? (0, format_1.padEnd)(_theme_1.t.dim('MEM'), cols.memW) : '';
|
|
224
|
+
const ports = cols.showPorts ? _theme_1.t.dim('PORTS') : '';
|
|
225
|
+
this.header.setContent(`${indent}${name}${status}${cpu}${mem}${ports}`);
|
|
226
|
+
}
|
|
227
|
+
renderRow(row, cols) {
|
|
228
|
+
return row.kind === 'stack-header' ? this.renderStackHeader(row, cols) : this.renderService(row, cols);
|
|
229
|
+
}
|
|
230
|
+
renderStackHeader(row, cols) {
|
|
231
|
+
const caret = row.expanded ? _theme_1.t.accent('▾') : _theme_1.t.accent('▸');
|
|
232
|
+
const indent = (0, format_1.padEnd)(` ${caret}`, cols.indentW);
|
|
233
|
+
const glyph = _theme_1.t.accent('▣');
|
|
234
|
+
const labelMax = cols.nameW + cols.imageW - 4;
|
|
235
|
+
const name = (0, format_1.truncate)(row.stack.id, labelMax);
|
|
236
|
+
const nameRender = row.stack.isLive ? `{bold}${_theme_1.t.fg(name)}{/bold}` : `{bold}${_theme_1.t.dim(name)}{/bold}`;
|
|
237
|
+
const counts = row.stack.counts;
|
|
238
|
+
const pills = [];
|
|
239
|
+
if (counts.running > 0)
|
|
240
|
+
pills.push(this.pill(`${counts.running} up`, _theme_1.t.green));
|
|
241
|
+
if (counts.errored > 0)
|
|
242
|
+
pills.push(this.pill(`${counts.errored} err`, _theme_1.t.red));
|
|
243
|
+
if (counts.stopped > 0)
|
|
244
|
+
pills.push(this.pill(`${counts.stopped} stop`, _theme_1.t.faint));
|
|
245
|
+
const nameCol = (0, format_1.padEnd)(`${glyph} ${nameRender} ${pills.join(' ')}`, cols.nameW + cols.imageW + 1);
|
|
246
|
+
const stateLabel = row.stack.isLive ? _theme_1.t.aqua('live') : _theme_1.t.faint('—');
|
|
247
|
+
const status = (0, format_1.padEnd)(stateLabel, cols.statusW);
|
|
248
|
+
const cpu = cols.showCpu ? (0, format_1.padEnd)('', cols.cpuW) : '';
|
|
249
|
+
const mem = cols.showMem ? (0, format_1.padEnd)('', cols.memW) : '';
|
|
250
|
+
const ports = cols.showPorts ? '' : '';
|
|
251
|
+
return `${indent}${nameCol}${status}${cpu}${mem}${ports}`;
|
|
252
|
+
}
|
|
253
|
+
renderService(row, cols) {
|
|
254
|
+
const c = row.container;
|
|
255
|
+
const indent = (0, format_1.padEnd)(` ${_theme_1.t.faint('└─')}`, cols.indentW);
|
|
256
|
+
const dot = (0, status_1.statusDot)(c);
|
|
257
|
+
const nameMax = Math.max(4, cols.nameW - 3);
|
|
258
|
+
const name = (0, format_1.truncate)(c.name, nameMax);
|
|
259
|
+
const nameStyled = (0, status_1.isActive)(c.status) ? _theme_1.t.fg(name) : _theme_1.t.dim(name);
|
|
260
|
+
const nameCol = (0, format_1.padEnd)(`${dot} ${nameStyled}`, cols.nameW);
|
|
261
|
+
const imageMax = Math.max(4, cols.imageW - 1);
|
|
262
|
+
const image = (0, format_1.truncate)(c.image, imageMax);
|
|
263
|
+
const imageCol = (0, format_1.padEnd)(_theme_1.t.dim(image), cols.imageW + 1);
|
|
264
|
+
const status = (0, format_1.padEnd)(this.colorStatus(c, shortStatus(c)), cols.statusW);
|
|
265
|
+
const cpuVal = this.statsCache.get(c.id)?.cpuPercent;
|
|
266
|
+
const memVal = this.statsCache.get(c.id)?.memPercent;
|
|
267
|
+
const cpuCol = cols.showCpu ? (0, format_1.padEnd)((0, status_1.formatCpuCell)(c, cpuVal), cols.cpuW) : '';
|
|
268
|
+
const memCol = cols.showMem ? (0, format_1.padEnd)((0, status_1.formatMemCell)(c, memVal), cols.memW) : '';
|
|
269
|
+
let portsCol = '';
|
|
270
|
+
if (cols.showPorts) {
|
|
271
|
+
const portStr = c.ports[0] ? (0, format_1.truncate)(c.ports[0], cols.portsW - 1) : '';
|
|
272
|
+
portsCol = portStr ? _theme_1.t.pink(portStr) : _theme_1.t.faint('—');
|
|
273
|
+
}
|
|
274
|
+
return `${indent}${nameCol}${imageCol}${status}${cpuCol}${memCol}${portsCol}`;
|
|
275
|
+
}
|
|
276
|
+
pill(text, color) {
|
|
277
|
+
return `${_theme_1.t.faint('[')}${color(text)}${_theme_1.t.faint(']')}`;
|
|
278
|
+
}
|
|
279
|
+
colorStatus(c, text) {
|
|
280
|
+
if ((0, status_1.isActive)(c.status))
|
|
281
|
+
return _theme_1.t.green(text);
|
|
282
|
+
if ((c.status === 'exited' || c.status === 'dead') && c.exitCode !== 0)
|
|
283
|
+
return _theme_1.t.red(text);
|
|
284
|
+
return _theme_1.t.dim(text);
|
|
285
|
+
}
|
|
286
|
+
currentSelectionKey() {
|
|
287
|
+
return this.rowKey(this.rows[(0, widgets_1.listSelected)(this.list)]);
|
|
288
|
+
}
|
|
289
|
+
rowKey(row) {
|
|
290
|
+
if (!row)
|
|
291
|
+
return null;
|
|
292
|
+
return row.kind === 'stack-header' ? `s:${row.stackId}` : `c:${row.container.id}`;
|
|
293
|
+
}
|
|
294
|
+
rowToSelection(row) {
|
|
295
|
+
if (!row)
|
|
296
|
+
return null;
|
|
297
|
+
if (row.kind === 'stack-header') {
|
|
298
|
+
return { kind: 'stack', stack: row.stack, expanded: row.expanded };
|
|
299
|
+
}
|
|
300
|
+
return { kind: 'service', container: row.container, stackId: row.stackId };
|
|
301
|
+
}
|
|
302
|
+
emitNavigate(idx) {
|
|
303
|
+
const sel = this.rowToSelection(this.rows[idx]);
|
|
304
|
+
this.navigateHandlers.forEach((h) => h(sel));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.StackTree = StackTree;
|
|
308
|
+
function shortRelative(s) {
|
|
309
|
+
if (/^a few seconds ago$/.test(s))
|
|
310
|
+
return 'now';
|
|
311
|
+
const m = s.match(/^(an?|\d+)\s+(second|minute|hour|day|month|year)s?\s+ago$/);
|
|
312
|
+
if (!m)
|
|
313
|
+
return s;
|
|
314
|
+
const num = m[1] === 'a' || m[1] === 'an' ? '1' : m[1];
|
|
315
|
+
const unit = m[2];
|
|
316
|
+
const suffix = unit === 'second'
|
|
317
|
+
? 's'
|
|
318
|
+
: unit === 'minute'
|
|
319
|
+
? 'm'
|
|
320
|
+
: unit === 'hour'
|
|
321
|
+
? 'h'
|
|
322
|
+
: unit === 'day'
|
|
323
|
+
? 'd'
|
|
324
|
+
: unit === 'month'
|
|
325
|
+
? 'mo'
|
|
326
|
+
: 'y';
|
|
327
|
+
return `${num}${suffix}`;
|
|
328
|
+
}
|
|
329
|
+
function shortStatus(c) {
|
|
330
|
+
switch (c.status) {
|
|
331
|
+
case 'running':
|
|
332
|
+
return `RUN ${c.uptime}`;
|
|
333
|
+
case 'paused':
|
|
334
|
+
return 'PAUSED';
|
|
335
|
+
case 'restarting':
|
|
336
|
+
return 'RESTARTING';
|
|
337
|
+
case 'exited':
|
|
338
|
+
return c.exitCode === 0 ? `EXIT 0 · ${shortRelative(c.uptime)}` : `EXIT ${c.exitCode}`;
|
|
339
|
+
case 'dead':
|
|
340
|
+
return c.exitCode !== 0 ? `DEAD ${c.exitCode}` : 'DEAD';
|
|
341
|
+
case 'created':
|
|
342
|
+
return 'NEW';
|
|
343
|
+
case 'removing':
|
|
344
|
+
return 'RMV';
|
|
345
|
+
}
|
|
346
|
+
}
|