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.
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.countContainersByNetwork = countContainersByNetwork;
4
+ exports.toNetworkInfo = toNetworkInfo;
5
+ exports.listNetworks = listNetworks;
6
+ exports.removeNetwork = removeNetwork;
7
+ const client_1 = require("./client");
8
+ // Docker's predefined networks can never be removed.
9
+ const BUILTIN_NETWORKS = new Set(['bridge', 'host', 'none']);
10
+ /**
11
+ * Tally how many containers are attached to each network, keyed by network NAME.
12
+ *
13
+ * We key by name (the `NetworkSettings.Networks` map key), not the endpoint's
14
+ * `NetworkID`: a stopped container can carry a *stale* NetworkID — the default
15
+ * `bridge` network is recreated with a fresh ID on daemon restart, so an exited
16
+ * container keeps pointing at the old ID and would never match the live network.
17
+ * Docker forbids two networks sharing a name on one daemon, so the name is a
18
+ * stable, unique key; it is also what the Containers view stores/shows
19
+ * (`toContainerInfo`), keeping the two views consistent. Counts every attached
20
+ * container regardless of state, like images/volumes derive their in-use flag
21
+ * from `listContainers({ all: true })`.
22
+ */
23
+ function countContainersByNetwork(rawContainers) {
24
+ const counts = new Map();
25
+ for (const c of rawContainers) {
26
+ for (const name of Object.keys(c.NetworkSettings?.Networks ?? {})) {
27
+ counts.set(name, (counts.get(name) ?? 0) + 1);
28
+ }
29
+ }
30
+ return counts;
31
+ }
32
+ function toNetworkInfo(raw, containerCount) {
33
+ return {
34
+ id: raw.Id,
35
+ name: raw.Name,
36
+ driver: raw.Driver ?? '',
37
+ scope: raw.Scope ?? '',
38
+ created: raw.Created ? new Date(raw.Created) : new Date(0),
39
+ containerCount,
40
+ inUse: containerCount > 0,
41
+ builtin: BUILTIN_NETWORKS.has(raw.Name),
42
+ };
43
+ }
44
+ async function listNetworks() {
45
+ try {
46
+ const [rawNetworks, rawContainers] = await Promise.all([
47
+ client_1.dockerode.listNetworks(),
48
+ client_1.dockerode.listContainers({ all: true }),
49
+ ]);
50
+ const counts = countContainersByNetwork(rawContainers);
51
+ return rawNetworks
52
+ .sort((a, b) => (a.Name ?? '').localeCompare(b.Name ?? ''))
53
+ .map((net) => toNetworkInfo(net, counts.get(net.Name) ?? 0));
54
+ }
55
+ catch (err) {
56
+ throw new Error(`Failed to list networks: ${err instanceof Error ? err.message : String(err)}`);
57
+ }
58
+ }
59
+ async function removeNetwork(id) {
60
+ try {
61
+ await client_1.dockerode.getNetwork(id).remove();
62
+ }
63
+ catch (err) {
64
+ throw new Error(`Failed to remove network ${id}: ${err instanceof Error ? err.message : String(err)}`);
65
+ }
66
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listVolumes = listVolumes;
4
+ exports.removeVolume = removeVolume;
5
+ const client_1 = require("./client");
6
+ async function listVolumes() {
7
+ try {
8
+ const [volumesResponse, rawContainers, dfData] = await Promise.all([
9
+ client_1.dockerode.listVolumes(),
10
+ client_1.dockerode.listContainers({ all: true }),
11
+ client_1.dockerode.df().catch(() => ({ Volumes: [] })),
12
+ ]);
13
+ const usedVolumeNames = new Set(rawContainers.flatMap((c) => c.Mounts.map((m) => m.Name ?? '').filter(Boolean)));
14
+ const df = dfData;
15
+ const dfSizes = new Map((df.Volumes ?? [])
16
+ .filter((v) => typeof v.Name === 'string')
17
+ .map((v) => [v.Name, v.UsageData?.Size ?? -1]));
18
+ return (volumesResponse.Volumes ?? [])
19
+ .sort((a, b) => a.Name.localeCompare(b.Name))
20
+ .map((vol) => {
21
+ const sizeBytes = dfSizes.get(vol.Name) ?? -1;
22
+ const createdAt = vol.CreatedAt;
23
+ return {
24
+ name: vol.Name,
25
+ driver: vol.Driver,
26
+ mountpoint: vol.Mountpoint,
27
+ created: createdAt ? new Date(createdAt) : new Date(0),
28
+ sizeMB: sizeBytes > 0 ? sizeBytes / 1024 / 1024 : 0,
29
+ inUse: usedVolumeNames.has(vol.Name),
30
+ };
31
+ });
32
+ }
33
+ catch (err) {
34
+ throw new Error(`Failed to list volumes: ${err instanceof Error ? err.message : String(err)}`);
35
+ }
36
+ }
37
+ async function removeVolume(name) {
38
+ try {
39
+ await client_1.dockerode.getVolume(name).remove();
40
+ }
41
+ catch (err) {
42
+ throw new Error(`Failed to remove volume ${name}: ${err instanceof Error ? err.message : String(err)}`);
43
+ }
44
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/theme.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.t = exports.C = void 0;
4
+ exports.C = {
5
+ // backgrounds
6
+ bg: '#0a0908',
7
+ bgSoft: '#0f0d0c',
8
+ bgAlt: '#0f0d0c', // alias of bgSoft
9
+ panel: '#13110f',
10
+ bgSel: '#1a1840',
11
+ selection: '#1a1840', // alias of bgSel
12
+ bgSelEdge: '#6366f1',
13
+ // text
14
+ fg: '#e8e4d8',
15
+ dim: '#8a8782',
16
+ comment: '#8a8782', // alias of dim
17
+ faint: '#5a5853',
18
+ // rules / dividers
19
+ rule: '#231f1c',
20
+ rule2: '#2e2925',
21
+ // accents
22
+ accent: '#a5a0ff',
23
+ purple: '#6366f1',
24
+ green: '#5ed68a',
25
+ aqua: '#5eead4',
26
+ cyan: '#5eead4', // alias of aqua
27
+ yellow: '#f4c64a',
28
+ orange: '#fb923c',
29
+ red: '#f87171',
30
+ pink: '#f472b6',
31
+ };
32
+ const tag = (hex, text) => `{${hex}-fg}${text}{/}`;
33
+ exports.t = {
34
+ bg: (s) => tag(exports.C.bg, s),
35
+ bgSoft: (s) => tag(exports.C.bgSoft, s),
36
+ bgAlt: (s) => tag(exports.C.bgAlt, s),
37
+ panel: (s) => tag(exports.C.panel, s),
38
+ bgSel: (s) => tag(exports.C.bgSel, s),
39
+ selection: (s) => tag(exports.C.selection, s),
40
+ fg: (s) => tag(exports.C.fg, s),
41
+ dim: (s) => tag(exports.C.dim, s),
42
+ comment: (s) => tag(exports.C.comment, s),
43
+ faint: (s) => tag(exports.C.faint, s),
44
+ rule: (s) => tag(exports.C.rule, s),
45
+ rule2: (s) => tag(exports.C.rule2, s),
46
+ accent: (s) => tag(exports.C.accent, s),
47
+ purple: (s) => tag(exports.C.purple, s),
48
+ green: (s) => tag(exports.C.green, s),
49
+ aqua: (s) => tag(exports.C.aqua, s),
50
+ cyan: (s) => tag(exports.C.cyan, s),
51
+ yellow: (s) => tag(exports.C.yellow, s),
52
+ orange: (s) => tag(exports.C.orange, s),
53
+ red: (s) => tag(exports.C.red, s),
54
+ pink: (s) => tag(exports.C.pink, s),
55
+ };
package/dist/ui/app.js ADDED
@@ -0,0 +1,517 @@
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.App = void 0;
7
+ const neo_blessed_1 = __importDefault(require("neo-blessed"));
8
+ const client_1 = require("../docker/client");
9
+ const containers_1 = require("../docker/containers");
10
+ const images_1 = require("../docker/images");
11
+ const volumes_1 = require("../docker/volumes");
12
+ const networks_1 = require("../docker/networks");
13
+ const stacks_1 = require("../utils/stacks");
14
+ const status_1 = require("../utils/status");
15
+ const top_bar_1 = require("./top-bar");
16
+ const footer_1 = require("./footer");
17
+ const side_rail_1 = require("./side-rail");
18
+ const help_overlay_1 = require("./help-overlay");
19
+ const stacks_tab_1 = require("./stacks/stacks-tab");
20
+ const containers_tab_1 = require("./containers/containers-tab");
21
+ const images_tab_1 = require("./images/images-tab");
22
+ const volumes_tab_1 = require("./volumes/volumes-tab");
23
+ const networks_tab_1 = require("./networks/networks-tab");
24
+ const VIEW_ORDER = ['stacks', 'containers', 'images', 'volumes', 'networks'];
25
+ const msgOf = (err) => (err instanceof Error ? err.message : String(err));
26
+ class App {
27
+ screen;
28
+ topBar;
29
+ footer;
30
+ rail;
31
+ helpOverlay;
32
+ stacksTab;
33
+ containersTab;
34
+ imagesTab;
35
+ volumesTab;
36
+ networksTab;
37
+ activeView = 'stacks';
38
+ overlayOpen = false;
39
+ containers = [];
40
+ stacks = [];
41
+ imageCount = 0;
42
+ volumeCount = 0;
43
+ networkCount = 0;
44
+ aggregateStats = null;
45
+ pollTimers = [];
46
+ footerMsgTimer = null;
47
+ exitResolve = null;
48
+ pollingContainers = false;
49
+ pollingResources = false;
50
+ pollingStats = false;
51
+ constructor() {
52
+ this.screen = neo_blessed_1.default.screen({
53
+ smartCSR: true,
54
+ mouse: true,
55
+ fullUnicode: true,
56
+ title: 'dockza',
57
+ terminal: 'xterm-256color',
58
+ });
59
+ const tabDims = {
60
+ top: 1,
61
+ left: side_rail_1.RAIL_WIDTH,
62
+ width: `100%-${side_rail_1.RAIL_WIDTH}`,
63
+ height: '100%-2',
64
+ };
65
+ this.topBar = new top_bar_1.TopBar(this.screen);
66
+ this.footer = new footer_1.Footer(this.screen);
67
+ this.rail = new side_rail_1.SideRail(this.screen);
68
+ this.helpOverlay = new help_overlay_1.HelpOverlay(this.screen);
69
+ this.stacksTab = new stacks_tab_1.StacksTab(this.screen, tabDims);
70
+ this.containersTab = new containers_tab_1.ContainersTab(this.screen, tabDims);
71
+ this.imagesTab = new images_tab_1.ImagesTab(this.screen, tabDims);
72
+ this.volumesTab = new volumes_tab_1.VolumesTab(this.screen, tabDims);
73
+ this.networksTab = new networks_tab_1.NetworksTab(this.screen, tabDims);
74
+ this.wireEvents();
75
+ this.setupKeys();
76
+ this.screen.on('destroy', () => this.stopPolling());
77
+ this.screen.on('resize', () => this.handleResize());
78
+ const externalShutdown = () => this.shutdown();
79
+ process.once('SIGTERM', externalShutdown);
80
+ process.once('SIGHUP', externalShutdown);
81
+ }
82
+ wireEvents() {
83
+ this.helpOverlay.on('hide', () => {
84
+ this.overlayOpen = false;
85
+ this.render();
86
+ });
87
+ this.rail.on('view-change', (id) => {
88
+ if (!this.canSwitchView())
89
+ return;
90
+ this.showView(id);
91
+ });
92
+ this.rail.on('stack-jump', (stackId) => {
93
+ if (!this.canSwitchView())
94
+ return;
95
+ this.showView('stacks');
96
+ this.stacksTab.jumpToStack(stackId);
97
+ this.render();
98
+ });
99
+ this.stacksTab.on('error', (msg) => this.setFooterMessage(msg, 'red'));
100
+ this.stacksTab.on('stack-update', (stacks) => {
101
+ this.stacks = stacks;
102
+ this.rail.setStacks(stacks);
103
+ this.refreshTopBarCounters();
104
+ });
105
+ this.stacksTab.on('navigate', (sel) => {
106
+ if (this.activeView !== 'stacks')
107
+ return;
108
+ this.footer.setContext(this.contextForStacks(sel));
109
+ this.render();
110
+ });
111
+ this.stacksTab.on('detail-open', () => {
112
+ if (this.activeView === 'stacks') {
113
+ this.footer.setContext('detail');
114
+ this.render();
115
+ }
116
+ });
117
+ this.stacksTab.on('log-open', () => {
118
+ if (this.activeView === 'stacks') {
119
+ this.footer.setContext('log');
120
+ this.render();
121
+ }
122
+ });
123
+ this.stacksTab.on('log-follow-change', () => {
124
+ if (this.activeView === 'stacks') {
125
+ this.footer.setContext('log');
126
+ this.render();
127
+ }
128
+ });
129
+ this.containersTab.on('error', (msg) => this.setFooterMessage(msg, 'red'));
130
+ this.containersTab.on('select', (c) => {
131
+ if (this.activeView !== 'containers')
132
+ return;
133
+ this.footer.setContext(this.contextForContainer(c));
134
+ this.render();
135
+ });
136
+ this.containersTab.on('detail-open', () => {
137
+ if (this.activeView === 'containers') {
138
+ this.footer.setContext('detail');
139
+ this.render();
140
+ }
141
+ });
142
+ this.containersTab.on('log-open', () => {
143
+ if (this.activeView === 'containers') {
144
+ this.footer.setContext('log');
145
+ this.render();
146
+ }
147
+ });
148
+ this.containersTab.on('log-follow-change', () => {
149
+ if (this.activeView === 'containers') {
150
+ this.footer.setContext('log');
151
+ this.render();
152
+ }
153
+ });
154
+ this.imagesTab.on('error', (msg) => this.setFooterMessage(msg, 'red'));
155
+ this.imagesTab.on('navigate', () => {
156
+ if (this.activeView === 'images') {
157
+ this.footer.setContext('images');
158
+ this.render();
159
+ }
160
+ });
161
+ this.volumesTab.on('error', (msg) => this.setFooterMessage(msg, 'red'));
162
+ this.volumesTab.on('navigate', () => {
163
+ if (this.activeView === 'volumes') {
164
+ this.footer.setContext('volumes');
165
+ this.render();
166
+ }
167
+ });
168
+ this.networksTab.on('error', (msg) => this.setFooterMessage(msg, 'red'));
169
+ this.networksTab.on('navigate', () => {
170
+ if (this.activeView === 'networks') {
171
+ this.footer.setContext('networks');
172
+ this.render();
173
+ }
174
+ });
175
+ }
176
+ contextForStacks(sel) {
177
+ if (!sel)
178
+ return 'global';
179
+ if (sel.kind === 'stack')
180
+ return 'stacks-tree-stack';
181
+ return (0, status_1.isActive)(sel.container.status) ? 'stacks-tree-running' : 'stacks-tree-stopped';
182
+ }
183
+ contextForContainer(c) {
184
+ if (!c)
185
+ return 'containers-empty';
186
+ return (0, status_1.isActive)(c.status) ? 'containers-running' : 'containers-stopped';
187
+ }
188
+ toggleHelp = () => {
189
+ if (this.overlayOpen) {
190
+ this.helpOverlay.hide();
191
+ return;
192
+ }
193
+ if (this.isHelpBlocked())
194
+ return;
195
+ this.overlayOpen = true;
196
+ this.helpOverlay.show();
197
+ };
198
+ setupKeys() {
199
+ this.screen.key('tab', () => {
200
+ if (!this.canSwitchView())
201
+ return;
202
+ const idx = (VIEW_ORDER.indexOf(this.activeView) + 1) % VIEW_ORDER.length;
203
+ this.showView(VIEW_ORDER[idx]);
204
+ });
205
+ this.screen.key('S-tab', () => {
206
+ if (!this.canSwitchView())
207
+ return;
208
+ const idx = (VIEW_ORDER.indexOf(this.activeView) + VIEW_ORDER.length - 1) % VIEW_ORDER.length;
209
+ this.showView(VIEW_ORDER[idx]);
210
+ });
211
+ for (let i = 0; i < VIEW_ORDER.length; i++) {
212
+ const view = VIEW_ORDER[i];
213
+ this.screen.key(String(i + 1), () => {
214
+ if (!this.canSwitchView())
215
+ return;
216
+ this.showView(view);
217
+ });
218
+ }
219
+ this.screen.key(['h'], this.toggleHelp);
220
+ this.screen.key(['q', 'C-c'], () => {
221
+ if (this.overlayOpen || this.isModalOpen())
222
+ return;
223
+ this.shutdown();
224
+ });
225
+ }
226
+ canSwitchView() {
227
+ return !this.overlayOpen && !this.isModalOpen();
228
+ }
229
+ isModalOpen() {
230
+ return (this.stacksTab.isOverlayOpen() ||
231
+ this.containersTab.isOverlayOpen() ||
232
+ this.imagesTab.isConfirmOpen() ||
233
+ this.volumesTab.isConfirmOpen() ||
234
+ this.networksTab.isConfirmOpen() ||
235
+ this.isFilterOpen());
236
+ }
237
+ isFilterOpen() {
238
+ return this.stacksTab.isFilterOpen();
239
+ }
240
+ isHelpBlocked() {
241
+ return (this.stacksTab.isConfirmOpen() ||
242
+ this.containersTab.isConfirmOpen() ||
243
+ this.imagesTab.isConfirmOpen() ||
244
+ this.volumesTab.isConfirmOpen() ||
245
+ this.networksTab.isConfirmOpen() ||
246
+ this.isFilterOpen());
247
+ }
248
+ showView(id) {
249
+ if (this.activeView === id)
250
+ return;
251
+ this.hideTab(this.activeView);
252
+ this.activeView = id;
253
+ this.showTab(id);
254
+ this.rail.setActiveView(id);
255
+ this.footer.setContext(this.defaultContextFor(id));
256
+ this.render();
257
+ }
258
+ hideTab(id) {
259
+ switch (id) {
260
+ case 'stacks':
261
+ this.stacksTab.hide();
262
+ break;
263
+ case 'containers':
264
+ this.containersTab.hide();
265
+ break;
266
+ case 'images':
267
+ this.imagesTab.hide();
268
+ break;
269
+ case 'volumes':
270
+ this.volumesTab.hide();
271
+ break;
272
+ case 'networks':
273
+ this.networksTab.hide();
274
+ break;
275
+ }
276
+ }
277
+ showTab(id) {
278
+ switch (id) {
279
+ case 'stacks':
280
+ this.stacksTab.show();
281
+ break;
282
+ case 'containers':
283
+ this.containersTab.show();
284
+ break;
285
+ case 'images':
286
+ this.imagesTab.show();
287
+ break;
288
+ case 'volumes':
289
+ this.volumesTab.show();
290
+ break;
291
+ case 'networks':
292
+ this.networksTab.show();
293
+ break;
294
+ }
295
+ }
296
+ defaultContextFor(id) {
297
+ switch (id) {
298
+ case 'stacks':
299
+ return this.contextForStacks(this.stacksTab.getSelected());
300
+ case 'containers':
301
+ return this.contextForContainer(this.containersTab.getSelected());
302
+ case 'images':
303
+ return 'images';
304
+ case 'volumes':
305
+ return 'volumes';
306
+ case 'networks':
307
+ return 'networks';
308
+ }
309
+ }
310
+ render() {
311
+ this.screen.render();
312
+ }
313
+ setFooterMessage(text, color, duration = 3000) {
314
+ if (this.footerMsgTimer)
315
+ clearTimeout(this.footerMsgTimer);
316
+ this.footer.setMessage({ text, color });
317
+ this.render();
318
+ this.footerMsgTimer = setTimeout(() => {
319
+ this.footerMsgTimer = null;
320
+ this.footer.setMessage(null);
321
+ this.render();
322
+ }, duration);
323
+ }
324
+ async start() {
325
+ const dockerVersion = await (0, client_1.getDockerVersion)();
326
+ this.topBar.setSocketPath((0, client_1.getDockerSocketLabel)());
327
+ this.topBar.setDockerVersion(dockerVersion);
328
+ this.topBar.setCounters({ running: 0, errored: 0, stopped: 0 });
329
+ this.topBar.render();
330
+ this.rail.setActiveView('stacks');
331
+ this.rail.setCounts({ stacks: 0, containers: 0, images: 0, volumes: 0, networks: 0 });
332
+ this.footer.setContext('global');
333
+ this.footer.startTicker(() => this.render());
334
+ this.stacksTab.showLoading();
335
+ this.containersTab.showLoading();
336
+ this.imagesTab.showLoading();
337
+ this.volumesTab.showLoading();
338
+ this.networksTab.showLoading();
339
+ this.showTab('stacks');
340
+ this.render();
341
+ await this.pollContainers();
342
+ await this.pollResources();
343
+ this.pollTimers.push(setInterval(() => {
344
+ void this.pollContainers();
345
+ }, 5000), setInterval(() => {
346
+ void this.pollResources();
347
+ }, 10000), setInterval(() => {
348
+ void this.pollStats();
349
+ }, 2000));
350
+ return new Promise((resolve) => {
351
+ this.exitResolve = resolve;
352
+ });
353
+ }
354
+ async pollContainers() {
355
+ if (this.pollingContainers)
356
+ return;
357
+ this.pollingContainers = true;
358
+ try {
359
+ const containers = await (0, containers_1.listContainers)();
360
+ this.containers = containers;
361
+ this.stacks = (0, stacks_1.groupIntoStacks)(containers);
362
+ this.stacksTab.setData(containers, this.stacks);
363
+ this.containersTab.setData(containers);
364
+ this.rail.setStacks(this.stacks);
365
+ this.refreshTopBarCounters();
366
+ this.refreshRailCounts();
367
+ this.footer.noteRefresh();
368
+ this.render();
369
+ }
370
+ catch (err) {
371
+ this.setFooterMessage(err instanceof Error ? err.message : String(err), 'red');
372
+ }
373
+ finally {
374
+ this.pollingContainers = false;
375
+ }
376
+ }
377
+ async pollResources() {
378
+ if (this.pollingResources)
379
+ return;
380
+ this.pollingResources = true;
381
+ try {
382
+ // Settle each resource independently so one listing's failure doesn't blank the others.
383
+ const [imagesR, volumesR, networksR] = await Promise.allSettled([
384
+ (0, images_1.listImages)(),
385
+ (0, volumes_1.listVolumes)(),
386
+ (0, networks_1.listNetworks)(),
387
+ ]);
388
+ const errors = [];
389
+ if (imagesR.status === 'fulfilled') {
390
+ this.imageCount = imagesR.value.length;
391
+ this.imagesTab.setData(imagesR.value);
392
+ }
393
+ else {
394
+ errors.push(msgOf(imagesR.reason));
395
+ }
396
+ if (volumesR.status === 'fulfilled') {
397
+ this.volumeCount = volumesR.value.length;
398
+ this.volumesTab.setData(volumesR.value);
399
+ }
400
+ else {
401
+ errors.push(msgOf(volumesR.reason));
402
+ }
403
+ if (networksR.status === 'fulfilled') {
404
+ this.networkCount = networksR.value.length;
405
+ this.networksTab.setData(networksR.value);
406
+ }
407
+ else {
408
+ errors.push(msgOf(networksR.reason));
409
+ }
410
+ this.refreshRailCounts();
411
+ if (errors.length > 0) {
412
+ this.setFooterMessage(errors[0], 'red');
413
+ }
414
+ else {
415
+ this.footer.noteRefresh();
416
+ }
417
+ this.render();
418
+ }
419
+ catch (err) {
420
+ this.setFooterMessage(msgOf(err), 'red');
421
+ }
422
+ finally {
423
+ this.pollingResources = false;
424
+ }
425
+ }
426
+ async pollStats() {
427
+ if (this.pollingStats)
428
+ return;
429
+ this.pollingStats = true;
430
+ try {
431
+ await this.doPollStats();
432
+ }
433
+ finally {
434
+ this.pollingStats = false;
435
+ }
436
+ }
437
+ async doPollStats() {
438
+ const running = this.containers.filter((c) => (0, status_1.isActive)(c.status));
439
+ await Promise.allSettled(running.map(async (c) => {
440
+ try {
441
+ const stats = await (0, containers_1.fetchStats)(c.id);
442
+ this.stacksTab.updateStats(c.id, stats);
443
+ this.containersTab.updateStats(c.id, stats);
444
+ }
445
+ catch {
446
+ // ignore per-container stats errors
447
+ }
448
+ }));
449
+ if (running.length > 0) {
450
+ this.aggregateStats = this.stacksTab.getAggregateStats();
451
+ this.stacksTab.refreshStats();
452
+ this.containersTab.refreshListStats();
453
+ this.topBar.setStats(this.aggregateStats);
454
+ this.footer.noteRefresh();
455
+ }
456
+ else {
457
+ this.aggregateStats = null;
458
+ this.topBar.setStats(null);
459
+ }
460
+ this.render();
461
+ }
462
+ refreshTopBarCounters() {
463
+ let running = 0;
464
+ let errored = 0;
465
+ let stopped = 0;
466
+ for (const c of this.containers) {
467
+ if (c.status === 'running' || c.status === 'paused' || c.status === 'restarting')
468
+ running++;
469
+ else if ((c.status === 'exited' || c.status === 'dead') && c.exitCode !== 0)
470
+ errored++;
471
+ else
472
+ stopped++;
473
+ }
474
+ this.topBar.setCounters({ running, errored, stopped });
475
+ }
476
+ refreshRailCounts() {
477
+ this.rail.setCounts({
478
+ stacks: this.stacks.length,
479
+ containers: this.containers.length,
480
+ images: this.imageCount,
481
+ volumes: this.volumeCount,
482
+ networks: this.networkCount,
483
+ });
484
+ }
485
+ handleResize() {
486
+ this.stacksTab.redraw();
487
+ this.containersTab.setData(this.containers);
488
+ this.imagesTab.redraw();
489
+ this.volumesTab.redraw();
490
+ this.networksTab.redraw();
491
+ this.topBar.render();
492
+ this.footer.render();
493
+ this.render();
494
+ }
495
+ stopPolling() {
496
+ for (const timer of this.pollTimers)
497
+ clearInterval(timer);
498
+ this.pollTimers = [];
499
+ this.footer.stopTicker();
500
+ }
501
+ shutdown() {
502
+ this.stopPolling();
503
+ if (this.footerMsgTimer) {
504
+ clearTimeout(this.footerMsgTimer);
505
+ this.footerMsgTimer = null;
506
+ }
507
+ this.stacksTab.cleanup();
508
+ this.containersTab.cleanup();
509
+ this.screen.destroy();
510
+ if (this.exitResolve) {
511
+ const resolve = this.exitResolve;
512
+ this.exitResolve = null;
513
+ resolve();
514
+ }
515
+ }
516
+ }
517
+ exports.App = App;