cdd-cli 3.1.3 → 3.1.5
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/.eslintrc.json +30 -0
- package/.github/workflows/docs.yml +35 -0
- package/CHANGELOG.md +44 -4
- package/FIXES_APPLIED.md +145 -147
- package/README.md +26 -0
- package/dist/App.js +20 -3
- package/dist/components/ContainerCreationPrompt.js +27 -1
- package/dist/components/ContainerList.js +11 -15
- package/dist/components/ContainerRow.js +73 -14
- package/dist/components/ContainerSection.js +16 -2
- package/dist/components/Footer.js +7 -1
- package/dist/components/Header.js +17 -1
- package/dist/components/LogViewer.js +21 -3
- package/dist/components/MessageFeedback.js +15 -1
- package/dist/components/PromptField.js +31 -1
- package/dist/components/StatsBar.js +46 -8
- package/dist/components/UsageMenu.js +30 -1
- package/dist/helpers/actionHelpers.js +17 -6
- package/dist/helpers/constants.js +30 -0
- package/dist/helpers/dockerService/dockerService.js +19 -0
- package/dist/helpers/dockerService/serviceComponents/containerActions.js +194 -35
- package/dist/helpers/dockerService/serviceComponents/containerList.js +45 -8
- package/dist/helpers/dockerService/serviceComponents/containerLogs.js +16 -6
- package/dist/helpers/dockerService/serviceComponents/containerStats.js +28 -9
- package/dist/helpers/exitWithMessage.js +2 -1
- package/dist/helpers/logger.js +112 -0
- package/dist/helpers/safeCall.js +21 -0
- package/dist/helpers/validationHelpers.js +20 -1
- package/dist/hooks/creation/useContainerActions.js +2 -1
- package/dist/hooks/creation/useContainerCreation.js +5 -4
- package/dist/hooks/creation/useLogsViewer.js +3 -2
- package/dist/hooks/useContainers.js +6 -3
- package/dist/hooks/useControls.js +253 -122
- package/dist/hooks/useLogsStream.js +3 -2
- package/dist/index.js +1 -0
- package/docs/components_ContainerCreationPrompt.jsx.html +114 -0
- package/docs/components_ContainerRow.jsx.html +151 -0
- package/docs/components_ContainerSection.jsx.html +75 -0
- package/docs/components_Footer.jsx.html +66 -0
- package/docs/components_Header.jsx.html +84 -0
- package/docs/components_LogViewer.jsx.html +91 -0
- package/docs/components_MessageFeedback.jsx.html +76 -0
- package/docs/components_PromptField.jsx.html +99 -0
- package/docs/components_StatsBar.jsx.html +105 -0
- package/docs/components_UsageMenu.jsx.html +73 -0
- package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/global.html +6145 -0
- package/docs/helpers_actionHelpers.js.html +94 -0
- package/docs/helpers_constants.js.html +81 -0
- package/docs/helpers_dockerService_dockerService.js.html +79 -0
- package/docs/helpers_dockerService_serviceComponents_containerActions.js.html +153 -0
- package/docs/helpers_dockerService_serviceComponents_containerList.js.html +81 -0
- package/docs/helpers_dockerService_serviceComponents_containerLogs.js.html +85 -0
- package/docs/helpers_dockerService_serviceComponents_containerStats.js.html +107 -0
- package/docs/helpers_dockerService_serviceComponents_imageUtils.js.html +82 -0
- package/docs/helpers_exitWithMessage.js.html +77 -0
- package/docs/helpers_safeCall.js.html +68 -0
- package/docs/helpers_validationHelpers.js.html +96 -0
- package/docs/hooks_creation_useContainerActions.js.html +120 -0
- package/docs/hooks_creation_useContainerCreation.js.html +152 -0
- package/docs/hooks_creation_useLogsViewer.js.html +102 -0
- package/docs/hooks_useControls.js.html +298 -0
- package/docs/hooks_useLogsStream.js.html +82 -0
- package/docs/index.html +65 -0
- package/docs/index.js.html +65 -0
- package/docs/module-index.html +171 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/styles/jsdoc-default.css +358 -0
- package/docs/styles/prettify-jsdoc.css +111 -0
- package/docs/styles/prettify-tomorrow.css +132 -0
- package/fix-imports.cjs +33 -6
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/src/App.jsx +16 -2
- package/src/components/ContainerCreationPrompt.jsx +26 -0
- package/src/components/ContainerList.jsx +12 -13
- package/src/components/ContainerRow.jsx +55 -20
- package/src/components/ContainerSection.jsx +15 -1
- package/src/components/Footer.jsx +6 -1
- package/src/components/Header.jsx +16 -0
- package/src/components/LogViewer.jsx +19 -2
- package/src/components/MessageFeedback.jsx +14 -0
- package/src/components/PromptField.jsx +30 -0
- package/src/components/StatsBar.jsx +47 -8
- package/src/components/UsageMenu.jsx +38 -8
- package/src/helpers/actionHelpers.js +16 -7
- package/src/helpers/constants.js +30 -0
- package/src/helpers/dockerService/dockerService.js +19 -0
- package/src/helpers/dockerService/serviceComponents/containerActions.js +123 -12
- package/src/helpers/dockerService/serviceComponents/containerList.js +61 -23
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +20 -6
- package/src/helpers/dockerService/serviceComponents/containerStats.js +24 -5
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
- package/src/helpers/exitWithMessage.js +2 -1
- package/src/helpers/logger.js +113 -0
- package/src/helpers/safeCall.js +18 -0
- package/src/helpers/validationHelpers.js +21 -3
- package/src/hooks/creation/useContainerActions.js +4 -3
- package/src/hooks/creation/useContainerCreation.js +5 -4
- package/src/hooks/creation/useLogsViewer.js +2 -1
- package/src/hooks/useContainers.js +6 -3
- package/src/hooks/useControls.js +244 -91
- package/src/hooks/useLogsStream.js +2 -1
- package/src/index.js +1 -0
- package/test/actionHelpers.test.js +106 -0
- package/test/containerActions.test.js +138 -0
- package/test/containerList.test.js +79 -0
- package/test/jest.setup.js +7 -0
- package/test/safeCall.test.js +25 -0
- package/test/useContainerCreation.dom.test.js +97 -0
- package/test/validationHelpers.test.js +2 -2
- package/AUDIT_REPORT.md +0 -1004
- package/dist/cdd.bundle.js +0 -8
- package/dist/components/ContainerCreator.js +0 -81
- package/dist/components/StatsViewer.js +0 -0
- package/dist/helpers/dockerActions.js +0 -64
- package/dist/helpers/dockerService.js +0 -284
- package/dist/hooks/useStatsPolling.js +0 -54
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React hook to manage Docker containers state.
|
|
3
3
|
*
|
|
4
|
-
* @returns {
|
|
4
|
+
* @returns {{containers: Array<Object>}} An object containing the current
|
|
5
|
+
* containers array. The hook starts a periodic refresh and fetches the
|
|
6
|
+
* container list immediately on mount.
|
|
5
7
|
* @example
|
|
6
8
|
* // Use in a component
|
|
7
|
-
* const
|
|
9
|
+
* const { containers } = useContainers();
|
|
8
10
|
*/
|
|
9
11
|
import React, { useState, useEffect } from "react";
|
|
10
12
|
import { getContainers } from "../helpers/dockerService/serviceComponents/containerList";
|
|
13
|
+
import { REFRESH_INTERVALS } from "../helpers/constants.js";
|
|
11
14
|
|
|
12
15
|
export function useContainers() {
|
|
13
16
|
const [containers, setContainers] = useState([]);
|
|
@@ -15,7 +18,7 @@ export function useContainers() {
|
|
|
15
18
|
useEffect(() => {
|
|
16
19
|
const fetch = async () => setContainers(await getContainers());
|
|
17
20
|
fetch();
|
|
18
|
-
|
|
21
|
+
const timer = setInterval(fetch, REFRESH_INTERVALS.CONTAINER_LIST);
|
|
19
22
|
return () => clearInterval(timer);
|
|
20
23
|
}, []);
|
|
21
24
|
|
package/src/hooks/useControls.js
CHANGED
|
@@ -2,9 +2,11 @@ import React from "react";
|
|
|
2
2
|
import { useContainerActions } from "./creation/useContainerActions";
|
|
3
3
|
import { useContainerCreation } from "./creation/useContainerCreation";
|
|
4
4
|
import { useLogsViewer } from "./creation/useLogsViewer";
|
|
5
|
-
import { useInput } from "ink";
|
|
5
|
+
import { useInput, useApp } from "ink";
|
|
6
6
|
import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
|
|
7
|
+
import { EXIT_DELAY } from "../helpers/constants.js";
|
|
7
8
|
import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
|
|
9
|
+
import { logger } from "../helpers/logger.js";
|
|
8
10
|
|
|
9
11
|
// Principal hook to manage user inputs and control the app state
|
|
10
12
|
/**
|
|
@@ -29,12 +31,18 @@ import { createContainer as svcCreateContainer } from "../helpers/dockerService/
|
|
|
29
31
|
* @property {Object} creation - The creation hook API (setters and helpers)
|
|
30
32
|
* @property {Object} actions - The actions hook API (helpers to start/stop/remove)
|
|
31
33
|
* @property {Object} logsViewer - The logs viewer hook API
|
|
34
|
+
* @property {boolean} showDebugLogs - Whether the debug log panel is visible
|
|
35
|
+
* @property {Array<string>} debugLogs - Buffered log lines captured from the shared logger
|
|
36
|
+
* @property {boolean} confirmErase - Whether the erase confirmation dialog is active
|
|
32
37
|
*/
|
|
33
38
|
export function useControls(containers = []) {
|
|
34
39
|
const [selected, setSelected] = React.useState(0);
|
|
35
40
|
const [creatingContainer, setCreatingContainer] = React.useState(false);
|
|
36
41
|
const [confirmErase, setConfirmErase] = React.useState(false);
|
|
42
|
+
const [showDebugLogs, setShowDebugLogs] = React.useState(false);
|
|
43
|
+
const [debugLogs, setDebugLogs] = React.useState([]);
|
|
37
44
|
const total = containers.length;
|
|
45
|
+
const { exit } = useApp();
|
|
38
46
|
|
|
39
47
|
// Modular hooks
|
|
40
48
|
const actions = useContainerActions({ containers });
|
|
@@ -60,10 +68,10 @@ export function useControls(containers = []) {
|
|
|
60
68
|
if (Object.keys(ExposedPorts).length) options.ExposedPorts = ExposedPorts;
|
|
61
69
|
if (Object.keys(PortBindings).length) options.HostConfig = { PortBindings };
|
|
62
70
|
if (env.length) options.Env = env;
|
|
63
|
-
|
|
71
|
+
if (containerName) options.name = containerName;
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
actions.setMessage(`Creating container ${imageName}...`);
|
|
74
|
+
actions.setMessageColor("yellow");
|
|
67
75
|
try {
|
|
68
76
|
const id = await svcCreateContainer(imageName, options);
|
|
69
77
|
actions.setMessage(`Created container ${id}`);
|
|
@@ -83,90 +91,158 @@ export function useControls(containers = []) {
|
|
|
83
91
|
// Handler to exit logs (delegated to logsViewer)
|
|
84
92
|
const exitLogs = logsViewer.closeLogs;
|
|
85
93
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Pipe container logs into the viewer while applying a hard limit.
|
|
96
|
+
* @param {string} containerId - Container identifier used by Docker.
|
|
97
|
+
*/
|
|
98
|
+
const startLogsStream = React.useCallback((containerId) => {
|
|
99
|
+
getLogsStream(
|
|
100
|
+
containerId,
|
|
101
|
+
(data) => logsViewer.setLogs((prev) => {
|
|
102
|
+
const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
|
|
103
|
+
return newLogs.slice(-1000);
|
|
104
|
+
}),
|
|
105
|
+
() => {},
|
|
106
|
+
(err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
|
|
107
|
+
);
|
|
108
|
+
}, [logsViewer]);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Manage the erase confirmation short-circuit flow.
|
|
112
|
+
*/
|
|
113
|
+
const processEraseConfirmation = React.useCallback((input, key) => {
|
|
114
|
+
if (input === "y" || input === "Y") {
|
|
115
|
+
actions.handleAction({
|
|
116
|
+
actionFn: async (id) => await actions.removeContainer(id),
|
|
117
|
+
actionLabel: "Erasing",
|
|
118
|
+
selected,
|
|
119
|
+
});
|
|
120
|
+
setConfirmErase(false);
|
|
121
|
+
actions.setMessageColor("yellow");
|
|
122
|
+
return;
|
|
108
123
|
}
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
125
|
+
if (input === "n" || input === "N" || key.escape) {
|
|
126
|
+
setConfirmErase(false);
|
|
127
|
+
actions.setMessage("");
|
|
128
|
+
actions.setMessageColor("");
|
|
115
129
|
return;
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Printable character input (append)
|
|
144
|
-
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
145
|
-
if (step === 0) appendCharToField(creation.setImageName, creation.imageName, input);
|
|
146
|
-
if (step === 1) appendCharToField(creation.setContainerName, creation.containerName, input);
|
|
147
|
-
if (step === 2) appendCharToField(creation.setPortInput, creation.portInput, input);
|
|
148
|
-
if (step === 3) appendCharToField(creation.setEnvInput, creation.envInput, input);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
// Otherwise ignore
|
|
132
|
+
actions.setMessage("Are you sure you want to erase this container? (y/n)");
|
|
133
|
+
actions.setMessageColor("yellow");
|
|
134
|
+
}, [actions, selected]);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle inputs while the logs viewer is active.
|
|
138
|
+
*/
|
|
139
|
+
const processLogsInput = React.useCallback((input, key) => {
|
|
140
|
+
if (input === "q" || key.escape) {
|
|
141
|
+
logsViewer.closeLogs();
|
|
142
|
+
}
|
|
143
|
+
}, [logsViewer]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Route keystrokes to the container creation wizard.
|
|
147
|
+
*/
|
|
148
|
+
const processCreationInput = React.useCallback((input, key) => {
|
|
149
|
+
const step = creation.step;
|
|
150
|
+
|
|
151
|
+
const removeLastChar = (setter) => setter((value) => (value || "").slice(0, -1));
|
|
152
|
+
const appendChar = (setter, current, ch) => setter((current || "") + ch);
|
|
153
|
+
|
|
154
|
+
if (key.escape) {
|
|
155
|
+
creation.cancelCreation();
|
|
156
|
+
setCreatingContainer(false);
|
|
152
157
|
return;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
if (input === "\r" || input === "\n") {
|
|
161
|
+
creation.nextStep();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (key.backspace || key.delete) {
|
|
166
|
+
if (step === 0) removeLastChar(creation.setImageName);
|
|
167
|
+
if (step === 1) removeLastChar(creation.setContainerName);
|
|
168
|
+
if (step === 2) removeLastChar(creation.setPortInput);
|
|
169
|
+
if (step === 3) removeLastChar(creation.setEnvInput);
|
|
164
170
|
return;
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
174
|
+
if (step === 0) appendChar(creation.setImageName, creation.imageName, input);
|
|
175
|
+
if (step === 1) appendChar(creation.setContainerName, creation.containerName, input);
|
|
176
|
+
if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
|
|
177
|
+
if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
|
|
178
|
+
}
|
|
179
|
+
}, [creation, setCreatingContainer]);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Navigate the container list using arrow keys.
|
|
183
|
+
* @returns {boolean} True when the input was consumed.
|
|
184
|
+
*/
|
|
185
|
+
const handleNavigation = React.useCallback((input, key) => {
|
|
186
|
+
if (total === 0) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (key.upArrow) {
|
|
191
|
+
setSelected((i) => (i === 0 ? total - 1 : i - 1));
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (key.downArrow) {
|
|
196
|
+
setSelected((i) => (i === total - 1 ? 0 : i + 1));
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return false;
|
|
201
|
+
}, [total]);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Handle the quit command.
|
|
205
|
+
* @returns {boolean} True when the input was consumed.
|
|
206
|
+
*/
|
|
207
|
+
const handleExitCommand = React.useCallback((input) => {
|
|
208
|
+
if (input !== "q") {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
actions.setMessage("Exiting...");
|
|
213
|
+
actions.setMessageColor("yellow");
|
|
214
|
+
logsViewer.closeLogs();
|
|
215
|
+
setShowDebugLogs(false);
|
|
216
|
+
|
|
217
|
+
const clearTerminal = () => {
|
|
218
|
+
try {
|
|
219
|
+
if (process.stdout && process.stdout.isTTY) {
|
|
220
|
+
process.stdout.write("\u001Bc");
|
|
221
|
+
} else {
|
|
222
|
+
console.clear();
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
// Ignore clear errors to avoid blocking exit.
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
clearTerminal();
|
|
231
|
+
exit();
|
|
232
|
+
if (process.env.NODE_ENV !== "test") {
|
|
233
|
+
setTimeout(() => process.exit(0), 50);
|
|
234
|
+
}
|
|
235
|
+
}, EXIT_DELAY);
|
|
236
|
+
return true;
|
|
237
|
+
}, [actions, exit, logsViewer]);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Execute Docker-related commands and prompt transitions.
|
|
241
|
+
* @returns {boolean} True when the input was consumed.
|
|
242
|
+
*/
|
|
243
|
+
const handleDockerCommands = React.useCallback((input) => {
|
|
244
|
+
const container = containers[selected];
|
|
245
|
+
|
|
170
246
|
if (input === "i") {
|
|
171
247
|
actions.handleAction({
|
|
172
248
|
actionFn: async (id) => await actions.startContainer(id),
|
|
@@ -174,7 +250,9 @@ export function useControls(containers = []) {
|
|
|
174
250
|
selected,
|
|
175
251
|
stateCheck: (c) => (c.state === "running" || c.status === "running") && "Container is already running."
|
|
176
252
|
});
|
|
253
|
+
return true;
|
|
177
254
|
}
|
|
255
|
+
|
|
178
256
|
if (input === "p") {
|
|
179
257
|
actions.handleAction({
|
|
180
258
|
actionFn: async (id) => await actions.stopContainer(id),
|
|
@@ -182,34 +260,39 @@ export function useControls(containers = []) {
|
|
|
182
260
|
selected,
|
|
183
261
|
stateCheck: (c) => (c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped") && "Container is already stopped."
|
|
184
262
|
});
|
|
263
|
+
return true;
|
|
185
264
|
}
|
|
265
|
+
|
|
186
266
|
if (input === "r") {
|
|
187
267
|
actions.handleAction({
|
|
188
268
|
actionFn: async (id) => await actions.restartContainer(id),
|
|
189
269
|
actionLabel: "Restarting",
|
|
190
270
|
selected,
|
|
191
271
|
});
|
|
272
|
+
return true;
|
|
192
273
|
}
|
|
193
|
-
|
|
274
|
+
|
|
275
|
+
if (input === "e") {
|
|
276
|
+
if (!container) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
194
280
|
setConfirmErase(true);
|
|
195
281
|
actions.setMessage("Are you sure you want to erase this container? (y/n)");
|
|
196
282
|
actions.setMessageColor("yellow");
|
|
197
|
-
return;
|
|
283
|
+
return true;
|
|
198
284
|
}
|
|
199
|
-
|
|
285
|
+
|
|
286
|
+
if (input === "l") {
|
|
287
|
+
if (!container) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
200
291
|
logsViewer.openLogs();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
(data) => logsViewer.setLogs((prev) => {
|
|
204
|
-
const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
|
|
205
|
-
// Limit to last 1000 lines to prevent memory leak
|
|
206
|
-
return newLogs.slice(-1000);
|
|
207
|
-
}),
|
|
208
|
-
() => {},
|
|
209
|
-
(err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
|
|
210
|
-
);
|
|
211
|
-
return;
|
|
292
|
+
startLogsStream(container.id);
|
|
293
|
+
return true;
|
|
212
294
|
}
|
|
295
|
+
|
|
213
296
|
if (input === "c") {
|
|
214
297
|
setCreatingContainer(true);
|
|
215
298
|
creation.setStep(0);
|
|
@@ -219,9 +302,77 @@ export function useControls(containers = []) {
|
|
|
219
302
|
creation.setEnvInput("");
|
|
220
303
|
creation.setMessage("Insert the name of the image to create: ");
|
|
221
304
|
creation.setMessageColor("yellow");
|
|
305
|
+
return true;
|
|
222
306
|
}
|
|
307
|
+
|
|
308
|
+
if (input === "d") {
|
|
309
|
+
setShowDebugLogs((prev) => !prev);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return false;
|
|
314
|
+
}, [actions, containers, creation, logsViewer, selected, setConfirmErase, setCreatingContainer, startLogsStream, setShowDebugLogs]);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Default handler executed when no special mode is active.
|
|
318
|
+
*/
|
|
319
|
+
const processGlobalInput = React.useCallback((input, key) => {
|
|
320
|
+
if (handleNavigation(input, key)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (handleExitCommand(input)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
handleDockerCommands(input);
|
|
329
|
+
}, [handleDockerCommands, handleExitCommand, handleNavigation]);
|
|
330
|
+
|
|
331
|
+
useInput((input, key) => {
|
|
332
|
+
if (confirmErase) {
|
|
333
|
+
processEraseConfirmation(input, key);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (logsViewer.showLogs) {
|
|
338
|
+
processLogsInput(input, key);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (creatingContainer) {
|
|
343
|
+
processCreationInput(input, key);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (showDebugLogs && key.escape) {
|
|
348
|
+
setShowDebugLogs(false);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
processGlobalInput(input, key);
|
|
223
353
|
});
|
|
224
354
|
|
|
355
|
+
// Mirror global logger entries so the D shortcut can surface them without leaving the CLI.
|
|
356
|
+
React.useEffect(() => {
|
|
357
|
+
const unsubscribe = logger.subscribe((entry) => {
|
|
358
|
+
setDebugLogs((prev) => {
|
|
359
|
+
const extras = (entry.args || []).map((arg) => {
|
|
360
|
+
if (typeof arg === "string") return arg;
|
|
361
|
+
if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
|
|
362
|
+
try {
|
|
363
|
+
return JSON.stringify(arg);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return "[unserializable]";
|
|
366
|
+
}
|
|
367
|
+
}).filter(Boolean);
|
|
368
|
+
const line = extras.length ? `${entry.formatted} ${extras.join(" ")}` : entry.formatted;
|
|
369
|
+
const next = [...prev, line];
|
|
370
|
+
return next.slice(-200);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
return unsubscribe;
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
225
376
|
return {
|
|
226
377
|
selected,
|
|
227
378
|
setSelected,
|
|
@@ -242,5 +393,7 @@ export function useControls(containers = []) {
|
|
|
242
393
|
actions,
|
|
243
394
|
logsViewer,
|
|
244
395
|
confirmErase,
|
|
396
|
+
showDebugLogs,
|
|
397
|
+
debugLogs,
|
|
245
398
|
};
|
|
246
399
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useCallback } from "react";
|
|
2
2
|
import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs";
|
|
3
|
+
import { safeCall } from "../helpers/safeCall";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Hook that manages opening and closing a logs stream for a container.
|
|
@@ -21,7 +22,7 @@ export function useLogsStream() {
|
|
|
21
22
|
|
|
22
23
|
const closeLogs = useCallback(() => {
|
|
23
24
|
if (logsStreamRef.current) {
|
|
24
|
-
logsStreamRef.current.destroy?.();
|
|
25
|
+
safeCall(logsStreamRef.current.destroy?.bind(logsStreamRef.current));
|
|
25
26
|
logsStreamRef.current = null;
|
|
26
27
|
}
|
|
27
28
|
}, []);
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { handleAction } from '../src/helpers/actionHelpers.js';
|
|
3
|
+
import { MESSAGE_TIMEOUTS } from '../src/helpers/constants.js';
|
|
4
|
+
|
|
5
|
+
describe('handleAction', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
jest.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.useRealTimers();
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('returns early when no container selected', async () => {
|
|
16
|
+
const setMessage = jest.fn();
|
|
17
|
+
const setMessageColor = jest.fn();
|
|
18
|
+
const actionFn = jest.fn();
|
|
19
|
+
|
|
20
|
+
await handleAction({
|
|
21
|
+
containers: [],
|
|
22
|
+
selected: 0,
|
|
23
|
+
actionFn,
|
|
24
|
+
actionLabel: 'Start',
|
|
25
|
+
setMessage,
|
|
26
|
+
setMessageColor,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(actionFn).not.toHaveBeenCalled();
|
|
30
|
+
expect(setMessage).not.toHaveBeenCalled();
|
|
31
|
+
expect(setMessageColor).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('uses stateCheck and shows error then clears message after SHORT timeout', async () => {
|
|
35
|
+
const setMessage = jest.fn();
|
|
36
|
+
const setMessageColor = jest.fn();
|
|
37
|
+
const actionFn = jest.fn();
|
|
38
|
+
|
|
39
|
+
const containers = [{ id: 'c1', name: 'one' }];
|
|
40
|
+
|
|
41
|
+
const stateCheck = (_c) => 'Already running';
|
|
42
|
+
|
|
43
|
+
await handleAction({
|
|
44
|
+
containers,
|
|
45
|
+
selected: 0,
|
|
46
|
+
actionFn,
|
|
47
|
+
actionLabel: 'Start',
|
|
48
|
+
setMessage,
|
|
49
|
+
setMessageColor,
|
|
50
|
+
stateCheck,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(setMessage).toHaveBeenCalledWith('Already running');
|
|
54
|
+
expect(setMessageColor).toHaveBeenCalledWith('red');
|
|
55
|
+
expect(actionFn).not.toHaveBeenCalled();
|
|
56
|
+
|
|
57
|
+
// advance timers to allow message clear
|
|
58
|
+
jest.advanceTimersByTime(MESSAGE_TIMEOUTS.SHORT + 10);
|
|
59
|
+
expect(setMessage).toHaveBeenCalledWith('');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('on success sets success message and clears after DEFAULT timeout', async () => {
|
|
63
|
+
const setMessage = jest.fn();
|
|
64
|
+
const setMessageColor = jest.fn();
|
|
65
|
+
const actionFn = jest.fn().mockResolvedValue(undefined);
|
|
66
|
+
|
|
67
|
+
const containers = [{ id: 'c1', name: 'one' }];
|
|
68
|
+
|
|
69
|
+
await handleAction({
|
|
70
|
+
containers,
|
|
71
|
+
selected: 0,
|
|
72
|
+
actionFn,
|
|
73
|
+
actionLabel: 'Start',
|
|
74
|
+
setMessage,
|
|
75
|
+
setMessageColor,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(actionFn).toHaveBeenCalledWith('c1');
|
|
79
|
+
expect(setMessage).toHaveBeenCalledWith('Start container completed successfully');
|
|
80
|
+
expect(setMessageColor).toHaveBeenCalledWith('green');
|
|
81
|
+
|
|
82
|
+
jest.advanceTimersByTime(MESSAGE_TIMEOUTS.DEFAULT + 10);
|
|
83
|
+
expect(setMessage).toHaveBeenCalledWith('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('on failure sets failed message and color red', async () => {
|
|
87
|
+
const setMessage = jest.fn();
|
|
88
|
+
const setMessageColor = jest.fn();
|
|
89
|
+
const actionFn = jest.fn().mockRejectedValue(new Error('boom'));
|
|
90
|
+
|
|
91
|
+
const containers = [{ id: 'c1', name: 'one' }];
|
|
92
|
+
|
|
93
|
+
await handleAction({
|
|
94
|
+
containers,
|
|
95
|
+
selected: 0,
|
|
96
|
+
actionFn,
|
|
97
|
+
actionLabel: 'Stop',
|
|
98
|
+
setMessage,
|
|
99
|
+
setMessageColor,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(actionFn).toHaveBeenCalledWith('c1');
|
|
103
|
+
expect(setMessage).toHaveBeenCalledWith('Failed to stop container.');
|
|
104
|
+
expect(setMessageColor).toHaveBeenCalledWith('red');
|
|
105
|
+
});
|
|
106
|
+
});
|