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,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContainersTab = void 0;
|
|
4
|
+
const containers_1 = require("../../docker/containers");
|
|
5
|
+
const status_1 = require("../../utils/status");
|
|
6
|
+
const external_terminal_1 = require("../../utils/external-terminal");
|
|
7
|
+
const container_list_1 = require("../containers/container-list");
|
|
8
|
+
const container_detail_1 = require("../containers/container-detail");
|
|
9
|
+
const confirm_dialog_1 = require("../containers/confirm-dialog");
|
|
10
|
+
const log_viewer_1 = require("../containers/log-viewer");
|
|
11
|
+
class ContainersTab {
|
|
12
|
+
screen;
|
|
13
|
+
containerList;
|
|
14
|
+
containerDetail;
|
|
15
|
+
confirmDialog;
|
|
16
|
+
logViewer;
|
|
17
|
+
containers = [];
|
|
18
|
+
selectedIndex = 0;
|
|
19
|
+
active = false;
|
|
20
|
+
statsCache = new Map();
|
|
21
|
+
errorHandlers = [];
|
|
22
|
+
containerSelectHandlers = [];
|
|
23
|
+
detailOpenHandlers = [];
|
|
24
|
+
logOpenHandlers = [];
|
|
25
|
+
logFollowChangeHandlers = [];
|
|
26
|
+
handleEnter = () => {
|
|
27
|
+
if (!this.active || this.isOverlayOpen())
|
|
28
|
+
return;
|
|
29
|
+
const c = this.containerList.getSelected();
|
|
30
|
+
if (!c)
|
|
31
|
+
return;
|
|
32
|
+
this.containerDetail.show(c, this.statsCache.get(c.id));
|
|
33
|
+
this.detailOpenHandlers.forEach((h) => h());
|
|
34
|
+
};
|
|
35
|
+
handleL = () => {
|
|
36
|
+
if (!this.active || this.isModalOpen())
|
|
37
|
+
return;
|
|
38
|
+
const c = this.getActionTarget();
|
|
39
|
+
if (!c)
|
|
40
|
+
return;
|
|
41
|
+
if (this.containerDetail.isVisible())
|
|
42
|
+
this.containerDetail.hide();
|
|
43
|
+
this.logViewer.show(c);
|
|
44
|
+
this.logOpenHandlers.forEach((h) => h());
|
|
45
|
+
};
|
|
46
|
+
confirmAndRun(title, message, danger, action) {
|
|
47
|
+
if (this.containerDetail.isVisible())
|
|
48
|
+
this.containerDetail.hide();
|
|
49
|
+
this.confirmDialog.show({
|
|
50
|
+
title,
|
|
51
|
+
message,
|
|
52
|
+
danger,
|
|
53
|
+
onConfirm: () => {
|
|
54
|
+
void action()
|
|
55
|
+
.then(() => this.refresh())
|
|
56
|
+
.catch((err) => this.emitError(err))
|
|
57
|
+
.finally(() => {
|
|
58
|
+
this.containerList.focus();
|
|
59
|
+
this.screen.render();
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
onCancel: () => {
|
|
63
|
+
this.containerList.focus();
|
|
64
|
+
this.screen.render();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
handleS = () => {
|
|
69
|
+
if (!this.active || this.isModalOpen())
|
|
70
|
+
return;
|
|
71
|
+
const c = this.getActionTarget();
|
|
72
|
+
if (!c || !(0, status_1.isActive)(c.status))
|
|
73
|
+
return;
|
|
74
|
+
this.confirmAndRun('Stop container?', `${c.name} will be stopped.`, true, () => (0, containers_1.stopContainer)(c.id));
|
|
75
|
+
};
|
|
76
|
+
handleR = () => {
|
|
77
|
+
if (!this.active || this.isModalOpen())
|
|
78
|
+
return;
|
|
79
|
+
const c = this.getActionTarget();
|
|
80
|
+
if (!c || !(0, status_1.isActive)(c.status))
|
|
81
|
+
return;
|
|
82
|
+
this.confirmAndRun('Restart container?', `${c.name} will be restarted.`, false, () => (0, containers_1.restartContainer)(c.id));
|
|
83
|
+
};
|
|
84
|
+
handleK = () => {
|
|
85
|
+
if (!this.active || this.isModalOpen())
|
|
86
|
+
return;
|
|
87
|
+
const c = this.getActionTarget();
|
|
88
|
+
if (!c || !(0, status_1.isActive)(c.status))
|
|
89
|
+
return;
|
|
90
|
+
this.confirmAndRun('Kill container?', `${c.name} will be killed (SIGKILL).`, true, () => (0, containers_1.killContainer)(c.id));
|
|
91
|
+
};
|
|
92
|
+
handleShiftS = () => {
|
|
93
|
+
if (!this.active || this.isModalOpen())
|
|
94
|
+
return;
|
|
95
|
+
const c = this.getActionTarget();
|
|
96
|
+
if (!c) {
|
|
97
|
+
this.emitError('No container selected');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if ((0, status_1.isActive)(c.status)) {
|
|
101
|
+
this.emitError(`${c.name} is already ${c.status}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (c.status !== 'exited' && c.status !== 'created') {
|
|
105
|
+
this.emitError(`Cannot start ${c.name}: status is ${c.status}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (this.containerDetail.isVisible())
|
|
109
|
+
this.containerDetail.hide();
|
|
110
|
+
void (0, containers_1.startContainer)(c.id)
|
|
111
|
+
.then(() => this.refresh())
|
|
112
|
+
.catch((err) => this.emitError(err))
|
|
113
|
+
.finally(() => {
|
|
114
|
+
this.containerList.focus();
|
|
115
|
+
this.screen.render();
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
handleD = () => {
|
|
119
|
+
if (!this.active || this.isModalOpen())
|
|
120
|
+
return;
|
|
121
|
+
const c = this.getActionTarget();
|
|
122
|
+
if (!c || (0, status_1.isActive)(c.status))
|
|
123
|
+
return;
|
|
124
|
+
this.confirmAndRun('Remove container?', `${c.name} will be permanently removed.`, true, () => (0, containers_1.removeContainer)(c.id));
|
|
125
|
+
};
|
|
126
|
+
handleX = () => {
|
|
127
|
+
if (!this.active || this.isModalOpen())
|
|
128
|
+
return;
|
|
129
|
+
const c = this.getActionTarget();
|
|
130
|
+
if (!c || c.status !== 'running') {
|
|
131
|
+
if (c)
|
|
132
|
+
this.emitError(`Cannot exec into ${c.name}: container is not running`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.containerDetail.isVisible())
|
|
136
|
+
this.containerDetail.hide();
|
|
137
|
+
const result = (0, external_terminal_1.openExternalShell)(c.id);
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
this.emitError(result.error ?? `Failed to open external terminal for ${c.name}`);
|
|
140
|
+
}
|
|
141
|
+
this.screen.render();
|
|
142
|
+
};
|
|
143
|
+
constructor(screen, dims) {
|
|
144
|
+
this.screen = screen;
|
|
145
|
+
this.containerList = new container_list_1.ContainerList(screen, dims);
|
|
146
|
+
this.containerDetail = new container_detail_1.ContainerDetail(screen, dims);
|
|
147
|
+
this.confirmDialog = new confirm_dialog_1.ConfirmDialog(screen);
|
|
148
|
+
this.logViewer = new log_viewer_1.LogViewer(screen, dims);
|
|
149
|
+
this.containerList.on('select', (container) => {
|
|
150
|
+
const idx = this.containers.findIndex((c) => c.id === container.id);
|
|
151
|
+
if (idx >= 0)
|
|
152
|
+
this.selectedIndex = idx;
|
|
153
|
+
});
|
|
154
|
+
this.containerList.on('navigate', (container) => {
|
|
155
|
+
this.containerSelectHandlers.forEach((h) => h(container));
|
|
156
|
+
});
|
|
157
|
+
this.containerDetail.on('close', () => {
|
|
158
|
+
this.containerList.focus();
|
|
159
|
+
this.emitSelect();
|
|
160
|
+
this.screen.render();
|
|
161
|
+
});
|
|
162
|
+
this.logViewer.on('close', () => {
|
|
163
|
+
this.containerList.focus();
|
|
164
|
+
this.emitSelect();
|
|
165
|
+
this.screen.render();
|
|
166
|
+
});
|
|
167
|
+
this.logViewer.on('follow-change', (following) => {
|
|
168
|
+
this.logFollowChangeHandlers.forEach((h) => h(following));
|
|
169
|
+
});
|
|
170
|
+
this.containerList.hide();
|
|
171
|
+
}
|
|
172
|
+
on(event, handler) {
|
|
173
|
+
if (event === 'error')
|
|
174
|
+
this.errorHandlers.push(handler);
|
|
175
|
+
if (event === 'select')
|
|
176
|
+
this.containerSelectHandlers.push(handler);
|
|
177
|
+
if (event === 'detail-open')
|
|
178
|
+
this.detailOpenHandlers.push(handler);
|
|
179
|
+
if (event === 'log-open')
|
|
180
|
+
this.logOpenHandlers.push(handler);
|
|
181
|
+
if (event === 'log-follow-change') {
|
|
182
|
+
this.logFollowChangeHandlers.push(handler);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
show() {
|
|
186
|
+
this.active = true;
|
|
187
|
+
this.containerList.show();
|
|
188
|
+
this.containerList.focus();
|
|
189
|
+
this.screen.key(['enter'], this.handleEnter);
|
|
190
|
+
this.screen.key(['l'], this.handleL);
|
|
191
|
+
this.screen.key(['s'], this.handleS);
|
|
192
|
+
this.screen.key(['r'], this.handleR);
|
|
193
|
+
this.screen.key(['k'], this.handleK);
|
|
194
|
+
this.screen.key(['S-s'], this.handleShiftS);
|
|
195
|
+
this.screen.key(['d'], this.handleD);
|
|
196
|
+
this.screen.key(['x'], this.handleX);
|
|
197
|
+
this.emitSelect();
|
|
198
|
+
}
|
|
199
|
+
hide() {
|
|
200
|
+
this.active = false;
|
|
201
|
+
this.containerList.hide();
|
|
202
|
+
if (this.containerDetail.isVisible())
|
|
203
|
+
this.containerDetail.hide();
|
|
204
|
+
if (this.logViewer.isVisible())
|
|
205
|
+
this.logViewer.hide();
|
|
206
|
+
if (this.confirmDialog.isVisible())
|
|
207
|
+
this.confirmDialog.hide();
|
|
208
|
+
this.screen.removeKey('enter', this.handleEnter);
|
|
209
|
+
this.screen.removeKey('l', this.handleL);
|
|
210
|
+
this.screen.removeKey('s', this.handleS);
|
|
211
|
+
this.screen.removeKey('r', this.handleR);
|
|
212
|
+
this.screen.removeKey('k', this.handleK);
|
|
213
|
+
this.screen.removeKey('S-s', this.handleShiftS);
|
|
214
|
+
this.screen.removeKey('d', this.handleD);
|
|
215
|
+
this.screen.removeKey('x', this.handleX);
|
|
216
|
+
this.containerSelectHandlers.forEach((h) => h(null));
|
|
217
|
+
}
|
|
218
|
+
showLoading() {
|
|
219
|
+
this.containerList.showLoading();
|
|
220
|
+
}
|
|
221
|
+
isOverlayOpen() {
|
|
222
|
+
return this.isModalOpen() || this.containerDetail.isVisible();
|
|
223
|
+
}
|
|
224
|
+
isConfirmOpen() {
|
|
225
|
+
return this.confirmDialog.isVisible();
|
|
226
|
+
}
|
|
227
|
+
getSelected() {
|
|
228
|
+
return this.containerList.getSelected();
|
|
229
|
+
}
|
|
230
|
+
cleanup() {
|
|
231
|
+
if (this.logViewer.isVisible())
|
|
232
|
+
this.logViewer.hide();
|
|
233
|
+
if (this.containerDetail.isVisible())
|
|
234
|
+
this.containerDetail.hide();
|
|
235
|
+
if (this.confirmDialog.isVisible())
|
|
236
|
+
this.confirmDialog.hide();
|
|
237
|
+
}
|
|
238
|
+
refreshListStats() {
|
|
239
|
+
const cursorPos = this.containerList.getSelectedIndex();
|
|
240
|
+
const listStats = new Map();
|
|
241
|
+
for (const c of this.containers) {
|
|
242
|
+
const s = this.statsCache.get(c.id);
|
|
243
|
+
if (s)
|
|
244
|
+
listStats.set(c.id, { cpuPercent: s.cpuPercent, memPercent: s.memPercent });
|
|
245
|
+
}
|
|
246
|
+
this.containerList.setData(this.containers, listStats);
|
|
247
|
+
this.containerList.list.select(Math.min(cursorPos, Math.max(0, this.containers.length - 1)));
|
|
248
|
+
}
|
|
249
|
+
setData(containers) {
|
|
250
|
+
const cursorId = this.containerList.getSelected()?.id ?? this.containers[this.selectedIndex]?.id;
|
|
251
|
+
this.containers = containers;
|
|
252
|
+
if (cursorId) {
|
|
253
|
+
const newIdx = containers.findIndex((c) => c.id === cursorId);
|
|
254
|
+
this.selectedIndex = newIdx >= 0 ? newIdx : 0;
|
|
255
|
+
}
|
|
256
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, containers.length - 1));
|
|
257
|
+
const listStats = new Map();
|
|
258
|
+
for (const c of containers) {
|
|
259
|
+
const s = this.statsCache.get(c.id);
|
|
260
|
+
if (s)
|
|
261
|
+
listStats.set(c.id, { cpuPercent: s.cpuPercent, memPercent: s.memPercent });
|
|
262
|
+
}
|
|
263
|
+
const targetIdx = this.selectedIndex;
|
|
264
|
+
this.containerList.setData(containers, listStats);
|
|
265
|
+
this.containerList.list.select(targetIdx);
|
|
266
|
+
if (this.containerDetail.isVisible()) {
|
|
267
|
+
const detailId = this.containerDetail.getContainerId();
|
|
268
|
+
if (detailId) {
|
|
269
|
+
const updated = containers.find((c) => c.id === detailId);
|
|
270
|
+
if (updated) {
|
|
271
|
+
this.containerDetail.update(updated, this.statsCache.get(detailId));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.containerDetail.hide();
|
|
275
|
+
this.containerList.focus();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.emitSelect();
|
|
280
|
+
}
|
|
281
|
+
updateStats(id, stats) {
|
|
282
|
+
this.statsCache.set(id, stats);
|
|
283
|
+
if (this.containerDetail.isVisible() && this.containerDetail.getContainerId() === id) {
|
|
284
|
+
const container = this.containers.find((c) => c.id === id);
|
|
285
|
+
if (container)
|
|
286
|
+
this.containerDetail.update(container, stats);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async refresh() {
|
|
290
|
+
const containers = await (0, containers_1.listContainers)();
|
|
291
|
+
this.setData(containers);
|
|
292
|
+
}
|
|
293
|
+
isModalOpen() {
|
|
294
|
+
return this.confirmDialog.isVisible() || this.logViewer.isVisible();
|
|
295
|
+
}
|
|
296
|
+
/** Container targeted by list/detail actions (detail takes precedence when open). */
|
|
297
|
+
getActionTarget() {
|
|
298
|
+
if (this.containerDetail.isVisible()) {
|
|
299
|
+
const id = this.containerDetail.getContainerId();
|
|
300
|
+
if (id)
|
|
301
|
+
return this.containers.find((c) => c.id === id) ?? null;
|
|
302
|
+
}
|
|
303
|
+
return this.containerList.getSelected();
|
|
304
|
+
}
|
|
305
|
+
emitSelect() {
|
|
306
|
+
const container = this.containerList.getSelected();
|
|
307
|
+
this.containerSelectHandlers.forEach((h) => h(container));
|
|
308
|
+
}
|
|
309
|
+
emitError(err) {
|
|
310
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
311
|
+
this.errorHandlers.forEach((h) => h(msg));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
exports.ContainersTab = ContainersTab;
|
|
@@ -0,0 +1,190 @@
|
|
|
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.LogViewer = void 0;
|
|
7
|
+
const neo_blessed_1 = __importDefault(require("neo-blessed"));
|
|
8
|
+
const containers_1 = require("../../docker/containers");
|
|
9
|
+
const _theme_1 = require("../../theme");
|
|
10
|
+
class LogViewer {
|
|
11
|
+
screen;
|
|
12
|
+
wrapper;
|
|
13
|
+
headerBox;
|
|
14
|
+
logBox;
|
|
15
|
+
visible = false;
|
|
16
|
+
following = true;
|
|
17
|
+
activeStream = null;
|
|
18
|
+
streamGen = 0;
|
|
19
|
+
pending = Buffer.alloc(0);
|
|
20
|
+
containerName = '';
|
|
21
|
+
closeHandlers = [];
|
|
22
|
+
followChangeHandlers = [];
|
|
23
|
+
handleF = () => {
|
|
24
|
+
if (!this.visible)
|
|
25
|
+
return;
|
|
26
|
+
this.following = !this.following;
|
|
27
|
+
this.followChangeHandlers.forEach((h) => h(this.following));
|
|
28
|
+
this.screen.render();
|
|
29
|
+
};
|
|
30
|
+
handleG = () => {
|
|
31
|
+
if (!this.visible)
|
|
32
|
+
return;
|
|
33
|
+
this.logBox.setScrollPerc(0);
|
|
34
|
+
this.screen.render();
|
|
35
|
+
};
|
|
36
|
+
handleShiftG = () => {
|
|
37
|
+
if (!this.visible)
|
|
38
|
+
return;
|
|
39
|
+
this.following = true;
|
|
40
|
+
this.logBox.setScrollPerc(100);
|
|
41
|
+
this.followChangeHandlers.forEach((h) => h(this.following));
|
|
42
|
+
this.screen.render();
|
|
43
|
+
};
|
|
44
|
+
handleUp = () => {
|
|
45
|
+
if (!this.visible)
|
|
46
|
+
return;
|
|
47
|
+
this.following = false;
|
|
48
|
+
this.logBox.scroll(-1);
|
|
49
|
+
this.followChangeHandlers.forEach((h) => h(this.following));
|
|
50
|
+
this.screen.render();
|
|
51
|
+
};
|
|
52
|
+
handleClose = () => {
|
|
53
|
+
if (!this.visible)
|
|
54
|
+
return;
|
|
55
|
+
this.hide();
|
|
56
|
+
this.closeHandlers.forEach((h) => h());
|
|
57
|
+
};
|
|
58
|
+
constructor(screen, dims) {
|
|
59
|
+
this.screen = screen;
|
|
60
|
+
this.wrapper = neo_blessed_1.default.box({
|
|
61
|
+
parent: screen,
|
|
62
|
+
top: dims.top,
|
|
63
|
+
left: dims.left,
|
|
64
|
+
width: dims.width,
|
|
65
|
+
height: dims.height,
|
|
66
|
+
style: { bg: _theme_1.C.bg },
|
|
67
|
+
hidden: true,
|
|
68
|
+
});
|
|
69
|
+
this.headerBox = neo_blessed_1.default.box({
|
|
70
|
+
parent: this.wrapper,
|
|
71
|
+
top: 0,
|
|
72
|
+
left: 0,
|
|
73
|
+
height: 1,
|
|
74
|
+
width: '100%',
|
|
75
|
+
tags: true,
|
|
76
|
+
style: { bg: _theme_1.C.bgSel },
|
|
77
|
+
});
|
|
78
|
+
this.logBox = neo_blessed_1.default.log({
|
|
79
|
+
parent: this.wrapper,
|
|
80
|
+
top: 1,
|
|
81
|
+
left: 0,
|
|
82
|
+
height: '100%-1',
|
|
83
|
+
width: '100%',
|
|
84
|
+
scrollable: true,
|
|
85
|
+
mouse: true,
|
|
86
|
+
keys: true,
|
|
87
|
+
vi: true,
|
|
88
|
+
tags: true,
|
|
89
|
+
alwaysScroll: true,
|
|
90
|
+
// Cap retained lines so a chatty container can't grow this without bound.
|
|
91
|
+
scrollback: 5000,
|
|
92
|
+
scrollbar: { ch: '│', style: { fg: _theme_1.C.comment } },
|
|
93
|
+
style: { fg: _theme_1.C.fg, bg: _theme_1.C.bg },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
on(event, handler) {
|
|
97
|
+
if (event === 'close')
|
|
98
|
+
this.closeHandlers.push(handler);
|
|
99
|
+
if (event === 'follow-change')
|
|
100
|
+
this.followChangeHandlers.push(handler);
|
|
101
|
+
}
|
|
102
|
+
show(container) {
|
|
103
|
+
this.containerName = container.name;
|
|
104
|
+
this.following = true;
|
|
105
|
+
this.logBox.setContent('');
|
|
106
|
+
this.logBox.setScrollPerc(0);
|
|
107
|
+
this.visible = true;
|
|
108
|
+
this.wrapper.show();
|
|
109
|
+
this.logBox.focus();
|
|
110
|
+
this.updateHeader();
|
|
111
|
+
this.screen.key(['f'], this.handleF);
|
|
112
|
+
this.screen.key(['g'], this.handleG);
|
|
113
|
+
this.screen.key(['S-g'], this.handleShiftG);
|
|
114
|
+
this.screen.key(['up', 'k'], this.handleUp);
|
|
115
|
+
this.screen.key(['escape'], this.handleClose);
|
|
116
|
+
this.screen.render();
|
|
117
|
+
this.pending = Buffer.alloc(0);
|
|
118
|
+
const myGen = ++this.streamGen;
|
|
119
|
+
void (0, containers_1.streamLogs)(container.id, 200)
|
|
120
|
+
.then((stream) => {
|
|
121
|
+
if (myGen !== this.streamGen) {
|
|
122
|
+
stream.destroy?.();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.activeStream = stream;
|
|
126
|
+
stream.on('data', (chunk) => {
|
|
127
|
+
if (myGen !== this.streamGen)
|
|
128
|
+
return;
|
|
129
|
+
this.parseFrames(chunk);
|
|
130
|
+
if (this.following)
|
|
131
|
+
this.logBox.setScrollPerc(100);
|
|
132
|
+
this.screen.render();
|
|
133
|
+
});
|
|
134
|
+
stream.on('error', (err) => {
|
|
135
|
+
if (myGen !== this.streamGen)
|
|
136
|
+
return;
|
|
137
|
+
this.logBox.pushLine(_theme_1.t.red(`Stream error: ${err.message}`));
|
|
138
|
+
this.screen.render();
|
|
139
|
+
});
|
|
140
|
+
})
|
|
141
|
+
.catch((err) => {
|
|
142
|
+
if (myGen !== this.streamGen)
|
|
143
|
+
return;
|
|
144
|
+
this.logBox.pushLine(_theme_1.t.red(`Failed to open log stream: ${err instanceof Error ? err.message : String(err)}`));
|
|
145
|
+
this.screen.render();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
hide() {
|
|
149
|
+
this.visible = false;
|
|
150
|
+
this.streamGen++;
|
|
151
|
+
this.pending = Buffer.alloc(0);
|
|
152
|
+
if (this.activeStream) {
|
|
153
|
+
this.activeStream.destroy?.();
|
|
154
|
+
this.activeStream = null;
|
|
155
|
+
}
|
|
156
|
+
this.wrapper.hide();
|
|
157
|
+
this.screen.removeKey('f', this.handleF);
|
|
158
|
+
this.screen.removeKey('g', this.handleG);
|
|
159
|
+
this.screen.removeKey('S-g', this.handleShiftG);
|
|
160
|
+
this.screen.removeKey('up', this.handleUp);
|
|
161
|
+
this.screen.removeKey('k', this.handleUp);
|
|
162
|
+
this.screen.removeKey('escape', this.handleClose);
|
|
163
|
+
this.screen.render();
|
|
164
|
+
}
|
|
165
|
+
isVisible() {
|
|
166
|
+
return this.visible;
|
|
167
|
+
}
|
|
168
|
+
updateHeader() {
|
|
169
|
+
this.headerBox.setContent(` ${_theme_1.t.purple('LOGS')} — ${_theme_1.t.fg(this.containerName)}`);
|
|
170
|
+
}
|
|
171
|
+
parseFrames(chunk) {
|
|
172
|
+
const buf = this.pending.length > 0 ? Buffer.concat([this.pending, chunk]) : chunk;
|
|
173
|
+
let offset = 0;
|
|
174
|
+
while (offset + 8 <= buf.length) {
|
|
175
|
+
const type = buf[offset];
|
|
176
|
+
const size = buf.readUInt32BE(offset + 4);
|
|
177
|
+
if (offset + 8 + size > buf.length)
|
|
178
|
+
break;
|
|
179
|
+
const message = buf.slice(offset + 8, offset + 8 + size).toString('utf8');
|
|
180
|
+
offset += 8 + size;
|
|
181
|
+
for (const line of message.split('\n')) {
|
|
182
|
+
if (!line)
|
|
183
|
+
continue;
|
|
184
|
+
this.logBox.pushLine(type === 2 ? _theme_1.t.orange(line) : line);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.pending = offset < buf.length ? buf.slice(offset) : Buffer.alloc(0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.LogViewer = LogViewer;
|
|
@@ -0,0 +1,193 @@
|
|
|
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.Footer = void 0;
|
|
7
|
+
const neo_blessed_1 = __importDefault(require("neo-blessed"));
|
|
8
|
+
const _theme_1 = require("../theme");
|
|
9
|
+
const format_1 = require("../utils/format");
|
|
10
|
+
const HINTS = {
|
|
11
|
+
global: [
|
|
12
|
+
{ key: '↑↓', verb: 'nav' },
|
|
13
|
+
{ key: '↵', verb: 'select' },
|
|
14
|
+
{ key: '1-5', verb: 'view' },
|
|
15
|
+
{ key: 'h', verb: 'help' },
|
|
16
|
+
{ key: 'q', verb: 'quit' },
|
|
17
|
+
],
|
|
18
|
+
'stacks-tree-stack': [
|
|
19
|
+
{ key: '↑↓', verb: 'nav' },
|
|
20
|
+
{ key: '→←', verb: 'expand' },
|
|
21
|
+
{ key: '↵', verb: 'toggle' },
|
|
22
|
+
{ key: '/', verb: 'filter' },
|
|
23
|
+
{ key: 'h', verb: 'help' },
|
|
24
|
+
{ key: 'q', verb: 'quit' },
|
|
25
|
+
],
|
|
26
|
+
'stacks-tree-running': [
|
|
27
|
+
{ key: '↑↓', verb: 'nav' },
|
|
28
|
+
{ key: '↵', verb: 'detail' },
|
|
29
|
+
{ key: 'l', verb: 'logs' },
|
|
30
|
+
{ key: 'x', verb: 'shell' },
|
|
31
|
+
{ key: 's', verb: 'stop' },
|
|
32
|
+
{ key: 'r', verb: 'restart' },
|
|
33
|
+
{ key: 'k', verb: 'kill' },
|
|
34
|
+
{ key: '/', verb: 'filter' },
|
|
35
|
+
{ key: 'h', verb: 'help' },
|
|
36
|
+
],
|
|
37
|
+
'stacks-tree-stopped': [
|
|
38
|
+
{ key: '↑↓', verb: 'nav' },
|
|
39
|
+
{ key: '↵', verb: 'detail' },
|
|
40
|
+
{ key: 'l', verb: 'logs' },
|
|
41
|
+
{ key: 'S', verb: 'start' },
|
|
42
|
+
{ key: 'd', verb: 'remove' },
|
|
43
|
+
{ key: '/', verb: 'filter' },
|
|
44
|
+
{ key: 'h', verb: 'help' },
|
|
45
|
+
],
|
|
46
|
+
'containers-running': [
|
|
47
|
+
{ key: '↑↓', verb: 'nav' },
|
|
48
|
+
{ key: '↵', verb: 'detail' },
|
|
49
|
+
{ key: 'l', verb: 'logs' },
|
|
50
|
+
{ key: 'x', verb: 'shell' },
|
|
51
|
+
{ key: 's', verb: 'stop' },
|
|
52
|
+
{ key: 'r', verb: 'restart' },
|
|
53
|
+
{ key: 'k', verb: 'kill' },
|
|
54
|
+
{ key: 'h', verb: 'help' },
|
|
55
|
+
],
|
|
56
|
+
'containers-stopped': [
|
|
57
|
+
{ key: '↑↓', verb: 'nav' },
|
|
58
|
+
{ key: '↵', verb: 'detail' },
|
|
59
|
+
{ key: 'l', verb: 'logs' },
|
|
60
|
+
{ key: 'S', verb: 'start' },
|
|
61
|
+
{ key: 'd', verb: 'remove' },
|
|
62
|
+
{ key: 'h', verb: 'help' },
|
|
63
|
+
],
|
|
64
|
+
'containers-empty': [
|
|
65
|
+
{ key: '↑↓', verb: 'nav' },
|
|
66
|
+
{ key: '1-5', verb: 'view' },
|
|
67
|
+
{ key: 'h', verb: 'help' },
|
|
68
|
+
{ key: 'q', verb: 'quit' },
|
|
69
|
+
],
|
|
70
|
+
images: [
|
|
71
|
+
{ key: '↑↓', verb: 'nav' },
|
|
72
|
+
{ key: 'd', verb: 'delete' },
|
|
73
|
+
{ key: 'h', verb: 'help' },
|
|
74
|
+
{ key: 'q', verb: 'quit' },
|
|
75
|
+
],
|
|
76
|
+
volumes: [
|
|
77
|
+
{ key: '↑↓', verb: 'nav' },
|
|
78
|
+
{ key: 'd', verb: 'delete' },
|
|
79
|
+
{ key: 'h', verb: 'help' },
|
|
80
|
+
{ key: 'q', verb: 'quit' },
|
|
81
|
+
],
|
|
82
|
+
networks: [
|
|
83
|
+
{ key: '↑↓', verb: 'nav' },
|
|
84
|
+
{ key: 'd', verb: 'delete' },
|
|
85
|
+
{ key: 'h', verb: 'help' },
|
|
86
|
+
{ key: 'q', verb: 'quit' },
|
|
87
|
+
],
|
|
88
|
+
detail: [
|
|
89
|
+
{ key: 'e', verb: 'env' },
|
|
90
|
+
{ key: 'l', verb: 'logs' },
|
|
91
|
+
{ key: 's/r/k', verb: 'ctrl' },
|
|
92
|
+
{ key: '↑↓', verb: 'scroll' },
|
|
93
|
+
{ key: 'Esc', verb: 'close' },
|
|
94
|
+
],
|
|
95
|
+
log: [
|
|
96
|
+
{ key: 'f', verb: 'follow' },
|
|
97
|
+
{ key: 'g', verb: 'top' },
|
|
98
|
+
{ key: 'G', verb: 'bottom' },
|
|
99
|
+
{ key: '↑↓', verb: 'scroll' },
|
|
100
|
+
{ key: 'Esc', verb: 'close' },
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
class Footer {
|
|
104
|
+
box;
|
|
105
|
+
context = 'global';
|
|
106
|
+
message = null;
|
|
107
|
+
lastRefreshAt = null;
|
|
108
|
+
tickerHandle = null;
|
|
109
|
+
constructor(screen) {
|
|
110
|
+
this.box = neo_blessed_1.default.box({
|
|
111
|
+
parent: screen,
|
|
112
|
+
bottom: 0,
|
|
113
|
+
left: 0,
|
|
114
|
+
width: '100%',
|
|
115
|
+
height: 1,
|
|
116
|
+
tags: true,
|
|
117
|
+
style: { bg: _theme_1.C.bg, fg: _theme_1.C.fg },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
setContext(c) {
|
|
121
|
+
this.context = c;
|
|
122
|
+
this.render();
|
|
123
|
+
}
|
|
124
|
+
setMessage(message) {
|
|
125
|
+
this.message = message;
|
|
126
|
+
this.render();
|
|
127
|
+
}
|
|
128
|
+
noteRefresh() {
|
|
129
|
+
this.lastRefreshAt = Date.now();
|
|
130
|
+
this.render();
|
|
131
|
+
}
|
|
132
|
+
startTicker(onTick) {
|
|
133
|
+
if (this.tickerHandle)
|
|
134
|
+
return;
|
|
135
|
+
this.tickerHandle = setInterval(() => {
|
|
136
|
+
this.render();
|
|
137
|
+
onTick();
|
|
138
|
+
}, 1000);
|
|
139
|
+
}
|
|
140
|
+
stopTicker() {
|
|
141
|
+
if (this.tickerHandle) {
|
|
142
|
+
clearInterval(this.tickerHandle);
|
|
143
|
+
this.tickerHandle = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
render() {
|
|
147
|
+
const width = Number(this.box.width) || 80;
|
|
148
|
+
const right = this.buildRight();
|
|
149
|
+
const left = this.message ? this.buildMessage() : this.buildHints(width - (0, format_1.visualLength)(right) - 2);
|
|
150
|
+
const gap = Math.max(1, width - (0, format_1.visualLength)(left) - (0, format_1.visualLength)(right));
|
|
151
|
+
this.box.setContent(left + ' '.repeat(gap) + right);
|
|
152
|
+
}
|
|
153
|
+
buildMessage() {
|
|
154
|
+
if (!this.message)
|
|
155
|
+
return '';
|
|
156
|
+
const text = this.message.text;
|
|
157
|
+
if (this.message.color === 'red')
|
|
158
|
+
return ` ${_theme_1.t.red(text)}`;
|
|
159
|
+
if (this.message.color === 'green')
|
|
160
|
+
return ` ${_theme_1.t.green(text)}`;
|
|
161
|
+
return ` ${_theme_1.t.fg(text)}`;
|
|
162
|
+
}
|
|
163
|
+
buildHints(budget) {
|
|
164
|
+
const hints = HINTS[this.context];
|
|
165
|
+
const parts = [' '];
|
|
166
|
+
let visible = 1;
|
|
167
|
+
for (const h of hints) {
|
|
168
|
+
const piece = `${this.renderKey(h.key)} ${_theme_1.t.dim(h.verb)}`;
|
|
169
|
+
const pieceLen = (0, format_1.visualLength)(piece);
|
|
170
|
+
const separator = parts.length === 1 ? '' : ' ';
|
|
171
|
+
const sepLen = separator.length;
|
|
172
|
+
if (visible + pieceLen + sepLen > budget) {
|
|
173
|
+
if (parts.length > 1)
|
|
174
|
+
parts.push(_theme_1.t.faint('…'));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
parts.push(separator + piece);
|
|
178
|
+
visible += pieceLen + sepLen;
|
|
179
|
+
}
|
|
180
|
+
return parts.join('');
|
|
181
|
+
}
|
|
182
|
+
renderKey(key) {
|
|
183
|
+
return `{${_theme_1.C.rule2}-bg}{${_theme_1.C.fg}-fg} ${key} {/}`;
|
|
184
|
+
}
|
|
185
|
+
buildRight() {
|
|
186
|
+
if (!this.lastRefreshAt)
|
|
187
|
+
return `${_theme_1.t.faint('—')} `;
|
|
188
|
+
const ageSec = (Date.now() - this.lastRefreshAt) / 1000;
|
|
189
|
+
const label = ageSec < 1 ? `${ageSec.toFixed(1)}s` : `${Math.floor(ageSec)}s`;
|
|
190
|
+
return `${_theme_1.t.faint(`last refresh ${label} ago`)} `;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
exports.Footer = Footer;
|