cdd-cli 3.0.1 → 3.1.1
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/.github/ISSUE_TEMPLATE/bug_report.md +30 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +14 -0
- package/.github/workflows/ci.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/CODE_OF_CONDUCT.md +12 -0
- package/CONTRIBUTING.md +22 -0
- package/README.md +83 -166
- package/SECURITY.md +5 -0
- package/babel.config.cjs +6 -0
- package/dist/App.js +38 -24
- package/dist/cdd.bundle.js +8 -0
- package/dist/components/ContainerCreationPrompt.js +48 -0
- package/dist/components/ContainerCreator.js +81 -0
- package/dist/components/ContainerList.js +24 -4
- package/dist/components/ContainerRow.js +66 -13
- package/dist/components/ContainerSection.js +14 -0
- package/dist/components/Footer.js +10 -0
- package/dist/components/PromptField.js +20 -0
- package/dist/components/UsageMenu.js +8 -0
- package/dist/helpers/actionHelpers.js +0 -1
- package/dist/helpers/dockerActions.js +13 -0
- package/dist/helpers/dockerService/dockerService.js +5 -0
- package/dist/helpers/dockerService/serviceComponents/containerActions.js +163 -0
- package/dist/helpers/dockerService/serviceComponents/containerList.js +57 -0
- package/dist/helpers/dockerService/serviceComponents/containerLogs.js +24 -0
- package/dist/helpers/dockerService/serviceComponents/containerStats.js +50 -0
- package/dist/helpers/dockerService/serviceComponents/imageUtils.js +52 -0
- package/dist/helpers/dockerService.js +111 -0
- package/dist/helpers/validationHelpers.js +26 -0
- package/dist/hooks/creation/useContainerActions.js +171 -0
- package/dist/hooks/creation/useContainerCreation.js +141 -0
- package/dist/hooks/creation/useLogsViewer.js +62 -0
- package/dist/hooks/useContainers.js +11 -1
- package/dist/hooks/useControls.js +298 -78
- package/dist/hooks/useLogsStream.js +1 -1
- package/dist/index.js +11 -0
- package/esbuild.config.cjs +11 -0
- package/jest.config.cjs +6 -0
- package/package.json +10 -6
- package/src/App.jsx +32 -26
- package/src/components/ContainerCreationPrompt.jsx +37 -0
- package/src/components/ContainerList.jsx +10 -6
- package/src/components/ContainerRow.jsx +36 -19
- package/src/components/ContainerSection.jsx +10 -0
- package/src/components/Footer.jsx +10 -0
- package/src/components/PromptField.jsx +18 -0
- package/src/components/UsageMenu.jsx +17 -0
- package/src/helpers/actionHelpers.js +0 -1
- package/src/helpers/dockerService/dockerService.js +3 -0
- package/src/helpers/dockerService/serviceComponents/containerActions.js +52 -0
- package/src/helpers/dockerService/serviceComponents/containerList.js +26 -0
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +19 -0
- package/src/helpers/dockerService/serviceComponents/containerStats.js +34 -0
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +21 -0
- package/src/helpers/validationHelpers.js +16 -0
- package/src/hooks/creation/useContainerActions.js +68 -0
- package/src/hooks/creation/useContainerCreation.js +104 -0
- package/src/hooks/creation/useLogsViewer.js +50 -0
- package/src/hooks/useContainers.js +1 -1
- package/src/hooks/useControls.js +176 -55
- package/src/hooks/useLogsStream.js +1 -1
- package/test/validationHelpers.test.js +28 -0
- package/PLAN_KEYBINDINGS_Y_REALTIME.md +0 -260
- package/docs/en/README.md +0 -167
- package/src/components/StatsViewer.jsx +0 -0
- package/src/helpers/dockerActions.js +0 -52
- package/src/helpers/dockerService.js +0 -100
|
@@ -13,17 +13,21 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import React from "react";
|
|
15
15
|
import { Box, Text } from "ink";
|
|
16
|
-
import ContainerRow from "./ContainerRow";
|
|
16
|
+
import ContainerRow from "./ContainerRow.jsx";
|
|
17
17
|
|
|
18
18
|
export default function ContainerList({ containers, selected }) {
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
21
|
{containers.map((container, i) => (
|
|
22
|
-
<Box key={container.id} flexDirection="row" alignItems="
|
|
23
|
-
<
|
|
24
|
-
{i === selected ? "
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
<Box key={container.id} flexDirection="row" alignItems="flex-start">
|
|
23
|
+
<Box width={2} minWidth={2} justifyContent="flex-end">
|
|
24
|
+
<Text color={i === selected ? "green" : undefined}>
|
|
25
|
+
{i === selected ? "➤" : " "}
|
|
26
|
+
</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
<Box flexGrow={1} paddingLeft={1}>
|
|
29
|
+
<ContainerRow container={container} isSelected={i === selected} />
|
|
30
|
+
</Box>
|
|
27
31
|
</Box>
|
|
28
32
|
))}
|
|
29
33
|
</>
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import StatsBar from "./StatsBar";
|
|
3
|
+
import { getStats } from "../helpers/dockerService/serviceComponents/containerStats";
|
|
4
|
+
import StatsBar from "./StatsBar.jsx";
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
if (state === "running") return
|
|
9
|
-
if (state === "exited") return
|
|
10
|
-
if (state === "paused") return
|
|
11
|
-
return
|
|
6
|
+
const stateText = (state) => {
|
|
7
|
+
if (state === "running") return { text: "🟢 RUNNING", color: "green" };
|
|
8
|
+
if (state === "exited") return { text: "🔴 EXITED", color: "red" };
|
|
9
|
+
if (state === "paused") return { text: "🟠 PAUSED", color: "yellow" };
|
|
10
|
+
return { text: state.toUpperCase(), color: "gray" };
|
|
12
11
|
};
|
|
13
12
|
|
|
14
13
|
export default function ContainerRow({ container }) {
|
|
@@ -19,34 +18,52 @@ export default function ContainerRow({ container }) {
|
|
|
19
18
|
netIO: { rx: 0, tx: 0 },
|
|
20
19
|
});
|
|
21
20
|
|
|
21
|
+
// Formatear puertos
|
|
22
|
+
const formatPorts = (ports) => {
|
|
23
|
+
if (!ports || ports.length === 0) return "";
|
|
24
|
+
if (Array.isArray(ports)) {
|
|
25
|
+
return ports.map((p, i) => ` 🔗 ${p}`).join(" ");
|
|
26
|
+
}
|
|
27
|
+
return ` 🔗 ${ports}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const [statsError, setStatsError] = useState("");
|
|
22
31
|
useEffect(() => {
|
|
23
32
|
if (state !== "running") return;
|
|
24
33
|
const fetchStats = async () => {
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
try {
|
|
35
|
+
const s = await getStats(id);
|
|
36
|
+
setStats(s);
|
|
37
|
+
setStatsError("");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setStats({ cpuPercent: 0, memPercent: 0, netIO: { rx: 0, tx: 0 } });
|
|
40
|
+
setStatsError("Error fetching stats");
|
|
41
|
+
}
|
|
27
42
|
};
|
|
28
43
|
fetchStats();
|
|
29
44
|
const timer = setInterval(fetchStats, 1500);
|
|
30
45
|
return () => clearInterval(timer);
|
|
31
46
|
}, [id, state]);
|
|
32
47
|
|
|
48
|
+
const stateInfo = stateText(state);
|
|
33
49
|
return (
|
|
34
50
|
<Box flexDirection="column" marginBottom={1}>
|
|
35
|
-
<
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{"
|
|
41
|
-
{chalk.yellow(container.ports)}
|
|
42
|
-
{" "}
|
|
51
|
+
<Box>
|
|
52
|
+
<Text color="cyan">{name.padEnd(20)}</Text>
|
|
53
|
+
<Text color="gray">{image.padEnd(20)}</Text>
|
|
54
|
+
<Text color={stateInfo.color}>{stateInfo.text}</Text>
|
|
55
|
+
<Text color="yellow">{formatPorts(container.ports)}</Text>
|
|
56
|
+
{state === "running" && <Text> </Text>}
|
|
43
57
|
{state === "running" && (
|
|
44
58
|
<StatsBar
|
|
45
59
|
cpu={parseFloat(stats.cpuPercent)}
|
|
46
60
|
mem={parseFloat(stats.memPercent)}
|
|
47
61
|
/>
|
|
48
62
|
)}
|
|
49
|
-
|
|
63
|
+
{state === "running" && statsError && (
|
|
64
|
+
<Text color="red">{statsError}</Text>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
50
67
|
</Box>
|
|
51
68
|
);
|
|
52
69
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import ContainerList from "./ContainerList.jsx";
|
|
4
|
+
|
|
5
|
+
export default function ContainerSection({ containers, selected }) {
|
|
6
|
+
if (containers.length === 0) {
|
|
7
|
+
return <Text>No containers found</Text>;
|
|
8
|
+
}
|
|
9
|
+
return <ContainerList containers={containers} selected={selected} />;
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
|
|
4
|
+
export function PromptField({ label, value, required }) {
|
|
5
|
+
// If the field is required and empty, show in red
|
|
6
|
+
const isEmpty = required && !value.trim();
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Text>{label}</Text>
|
|
10
|
+
<Text color={isEmpty ? "red" : "cyan"}>{value}_</Text>
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PromptMessage({ message, color }) {
|
|
16
|
+
if (!message) return null;
|
|
17
|
+
return <Text color={color || "yellow"}>{message}</Text>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
export default function UsageMenu() {
|
|
5
|
+
return (
|
|
6
|
+
<Box flexDirection="column" marginTop={1}>
|
|
7
|
+
<Text>Use ↑/↓ for navigation</Text>
|
|
8
|
+
<Text>•I to initiate selected container</Text>
|
|
9
|
+
<Text>•P to stop selected container</Text>
|
|
10
|
+
<Text>•R to restart selected container</Text>
|
|
11
|
+
<Text>•C to create a container</Text>
|
|
12
|
+
<Text>•L to view logs of selected container</Text>
|
|
13
|
+
<Text>•E to remove selected container</Text>
|
|
14
|
+
<Text>•Q to quit</Text>
|
|
15
|
+
</Box>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export async function removeContainer(containerId) {
|
|
2
|
+
const container = docker.getContainer(containerId);
|
|
3
|
+
try {
|
|
4
|
+
await container.remove({ force: true });
|
|
5
|
+
} catch (err) {
|
|
6
|
+
throw new Error('Error removing container: ' + err.message);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
import { docker } from "../dockerService";
|
|
10
|
+
import { imageExists, pullImage } from "./imageUtils.js";
|
|
11
|
+
|
|
12
|
+
export async function createContainer(imageName, options = {}) {
|
|
13
|
+
let exists;
|
|
14
|
+
try {
|
|
15
|
+
exists = await imageExists(imageName);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
throw new Error('Error listing local images: ' + err.message);
|
|
18
|
+
}
|
|
19
|
+
if (!exists) {
|
|
20
|
+
try {
|
|
21
|
+
await pullImage(imageName);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw new Error('Could not pull image: ' + err.message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const createOpts = {
|
|
27
|
+
Image: imageName,
|
|
28
|
+
Tty: true,
|
|
29
|
+
...options,
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const container = await docker.createContainer(createOpts);
|
|
33
|
+
return container.id || container.Id;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
throw new Error('Error creating container: ' + err.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function startContainer(containerId) {
|
|
40
|
+
const container = docker.getContainer(containerId);
|
|
41
|
+
await container.start();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function stopContainer(containerId) {
|
|
45
|
+
const container = docker.getContainer(containerId);
|
|
46
|
+
await container.stop();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function restartContainer(containerId) {
|
|
50
|
+
const container = docker.getContainer(containerId);
|
|
51
|
+
await container.restart();
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { docker } from "../dockerService";
|
|
2
|
+
|
|
3
|
+
export async function getContainers() {
|
|
4
|
+
const containers = await docker.listContainers({ all: true });
|
|
5
|
+
return containers.map((container) => ({
|
|
6
|
+
id: container.Id,
|
|
7
|
+
name: container.Names[0].replace("/", ""),
|
|
8
|
+
image: container.Image,
|
|
9
|
+
state: container.State,
|
|
10
|
+
status: container.Status,
|
|
11
|
+
ports:
|
|
12
|
+
(() => {
|
|
13
|
+
const publicPorts = container.Ports.filter((port) => port.PublicPort).map(
|
|
14
|
+
(port) => `${port.PublicPort}:${port.PrivatePort}`
|
|
15
|
+
);
|
|
16
|
+
if (publicPorts.length > 0) {
|
|
17
|
+
return [...new Set(publicPorts)];
|
|
18
|
+
}
|
|
19
|
+
// Si no hay puertos públicos, mostrar los privados expuestos
|
|
20
|
+
const privatePorts = container.Ports.filter((port) => port.PrivatePort).map(
|
|
21
|
+
(port) => `:${port.PrivatePort}`
|
|
22
|
+
);
|
|
23
|
+
return [...new Set(privatePorts)];
|
|
24
|
+
})()
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { docker } from "../dockerService";
|
|
2
|
+
|
|
3
|
+
export function getLogsStream(containerId, onData, onEnd, onError) {
|
|
4
|
+
const container = docker.getContainer(containerId);
|
|
5
|
+
container.logs({
|
|
6
|
+
follow: true,
|
|
7
|
+
stdout: true,
|
|
8
|
+
stderr: true,
|
|
9
|
+
tail: 100
|
|
10
|
+
}, (err, stream) => {
|
|
11
|
+
if (err) {
|
|
12
|
+
onError?.(err);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
stream.on('data', chunk => onData?.(chunk.toString()));
|
|
16
|
+
stream.on('end', () => onEnd?.());
|
|
17
|
+
stream.on('error', err => onError?.(err));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { docker } from "../dockerService";
|
|
2
|
+
|
|
3
|
+
export async function getStats(containerId) {
|
|
4
|
+
const container = docker.getContainer(containerId);
|
|
5
|
+
const stream = await container.stats({ stream: false });
|
|
6
|
+
|
|
7
|
+
const cpuDelta =
|
|
8
|
+
stream.cpu_stats.cpu_usage.total_usage -
|
|
9
|
+
stream.precpu_stats.cpu_usage.total_usage;
|
|
10
|
+
const systemDelta =
|
|
11
|
+
stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
|
|
12
|
+
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 : 0;
|
|
13
|
+
|
|
14
|
+
const memUsage = stream.memory_stats.usage || 0;
|
|
15
|
+
const memLimit = stream.memory_stats.limit || 1;
|
|
16
|
+
const memPercent = (memUsage / memLimit) * 100;
|
|
17
|
+
|
|
18
|
+
const rx = stream.networks
|
|
19
|
+
? Object.values(stream.networks)
|
|
20
|
+
.map((n) => n.rx_bytes)
|
|
21
|
+
.reduce((a, b) => a + b, 0)
|
|
22
|
+
: 0;
|
|
23
|
+
const tx = stream.networks
|
|
24
|
+
? Object.values(stream.networks)
|
|
25
|
+
.map((n) => n.tx_bytes)
|
|
26
|
+
.reduce((a, b) => a + b, 0)
|
|
27
|
+
: 0;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
cpuPercent: cpuPercent.toFixed(1),
|
|
31
|
+
memPercent: memPercent.toFixed(1),
|
|
32
|
+
netIO: { rx, tx },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { docker } from "../dockerService";
|
|
2
|
+
|
|
3
|
+
export async function imageExists(imageName) {
|
|
4
|
+
const images = await docker.listImages();
|
|
5
|
+
return images.some(img =>
|
|
6
|
+
(img.RepoTags || []).includes(imageName) ||
|
|
7
|
+
(img.RepoDigests || []).some(d => d.includes(imageName))
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function pullImage(imageName) {
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
docker.pull(imageName, (err, stream) => {
|
|
14
|
+
if (err) return reject(new Error('Error pulling image: ' + err.message));
|
|
15
|
+
docker.modem.followProgress(stream, (pullErr) => {
|
|
16
|
+
if (pullErr) reject(new Error('Error during pull: ' + pullErr.message));
|
|
17
|
+
else resolve();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Reusable validations for ports and environment variables
|
|
2
|
+
|
|
3
|
+
export function validatePorts(portInput) {
|
|
4
|
+
const ports = portInput.split(",").map(p => p.trim()).filter(Boolean);
|
|
5
|
+
if (ports.length === 0) return false;
|
|
6
|
+
const invalid = ports.find(pair => {
|
|
7
|
+
const [host, cont] = pair.split(":");
|
|
8
|
+
return !host || !cont || isNaN(Number(host)) || isNaN(Number(cont));
|
|
9
|
+
});
|
|
10
|
+
return !invalid;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateEnvVars(envInput) {
|
|
14
|
+
// Future specific validations
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { startContainer as svcStartContainer, stopContainer as svcStopContainer, restartContainer as svcRestartContainer, removeContainer as svcRemoveContainer } from "../../helpers/dockerService/serviceComponents/containerActions.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook to manage container actions (start, stop, restart, remove).
|
|
6
|
+
* Handles feedback messages and exposes helpers for each action.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} params
|
|
9
|
+
* @param {Array} params.containers - List of containers
|
|
10
|
+
* @param {Function} params.onAction - Callback after action is performed
|
|
11
|
+
* @returns {Object} Action helpers and feedback state
|
|
12
|
+
*/
|
|
13
|
+
export function useContainerActions({ containers, onAction }) {
|
|
14
|
+
const [message, setMessage] = useState("");
|
|
15
|
+
const [messageColor, setMessageColor] = useState("yellow");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handles a generic container action and sets feedback.
|
|
19
|
+
* @param {Function} actionFn - The async action function (start, stop, etc)
|
|
20
|
+
* @param {string} actionLabel - Action label for feedback
|
|
21
|
+
* @param {number} selected - Index of selected container
|
|
22
|
+
* @param {Function} [stateCheck] - Optional function to check state before action
|
|
23
|
+
*/
|
|
24
|
+
async function handleAction({ actionFn, actionLabel, selected, stateCheck }) {
|
|
25
|
+
const container = containers[selected];
|
|
26
|
+
if (!container) return;
|
|
27
|
+
if (stateCheck) {
|
|
28
|
+
const checkMsg = stateCheck(container);
|
|
29
|
+
if (checkMsg) {
|
|
30
|
+
setMessage(checkMsg);
|
|
31
|
+
setMessageColor("yellow");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
setMessage(`${actionLabel} container...`);
|
|
36
|
+
setMessageColor("yellow");
|
|
37
|
+
try {
|
|
38
|
+
await actionFn(container.id);
|
|
39
|
+
setMessage(`${actionLabel} container successful.`);
|
|
40
|
+
setMessageColor("green");
|
|
41
|
+
if (onAction) onAction();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
setMessage(`Failed to ${actionLabel.toLowerCase()} container: ${err.message}`);
|
|
44
|
+
setMessageColor("red");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
message,
|
|
50
|
+
setMessage,
|
|
51
|
+
messageColor,
|
|
52
|
+
setMessageColor,
|
|
53
|
+
handleAction,
|
|
54
|
+
// Expose concrete actions so callers can use actions.startContainer(id)
|
|
55
|
+
startContainer: async (id) => {
|
|
56
|
+
return await svcStartContainer(id);
|
|
57
|
+
},
|
|
58
|
+
stopContainer: async (id) => {
|
|
59
|
+
return await svcStopContainer(id);
|
|
60
|
+
},
|
|
61
|
+
restartContainer: async (id) => {
|
|
62
|
+
return await svcRestartContainer(id);
|
|
63
|
+
},
|
|
64
|
+
removeContainer: async (id) => {
|
|
65
|
+
return await svcRemoveContainer(id);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { validatePorts, validateEnvVars } from "../../helpers/validationHelpers.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook to manage the container creation flow, step by step.
|
|
6
|
+
* Handles input, validation, and feedback for each creation step.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} params
|
|
9
|
+
* @param {Function} params.onCreate - Callback when creation is confirmed
|
|
10
|
+
* @param {Function} params.onCancel - Callback when creation is cancelled
|
|
11
|
+
* @param {Array<string>} params.dbImages - List of DB image names for env var warning
|
|
12
|
+
* @returns {Object} Creation state, setters, and helpers
|
|
13
|
+
*/
|
|
14
|
+
export function useContainerCreation({ onCreate, onCancel, dbImages = [] }) {
|
|
15
|
+
const [step, setStep] = useState(0); // 0: image, 1: name, 2: ports, 3: env
|
|
16
|
+
const [imageName, setImageName] = useState("");
|
|
17
|
+
const [containerName, setContainerName] = useState("");
|
|
18
|
+
const [portInput, setPortInput] = useState("");
|
|
19
|
+
const [envInput, setEnvInput] = useState("");
|
|
20
|
+
const [message, setMessage] = useState("");
|
|
21
|
+
const [messageColor, setMessageColor] = useState("yellow");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Advances to the next step, with validation and feedback.
|
|
25
|
+
*/
|
|
26
|
+
function nextStep() {
|
|
27
|
+
if (step === 0) {
|
|
28
|
+
if (!imageName.trim()) {
|
|
29
|
+
setMessage("Image name cannot be empty.");
|
|
30
|
+
setMessageColor("red");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setStep(1);
|
|
34
|
+
setMessage("Optional: Enter container name or leave empty and press Enter");
|
|
35
|
+
setMessageColor("yellow");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (step === 1) {
|
|
39
|
+
setStep(2);
|
|
40
|
+
setMessage("Optional: Enter ports (format 8080:80,443:443) or leave empty and press Enter");
|
|
41
|
+
setMessageColor("yellow");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (step === 2) {
|
|
45
|
+
if (!portInput.trim()) {
|
|
46
|
+
setMessage("You must specify at least one port to expose (e.g. 8080:80)");
|
|
47
|
+
setMessageColor("red");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!validatePorts(portInput)) {
|
|
51
|
+
setMessage("Port format must be host:container and both must be numbers (e.g. 8080:80)");
|
|
52
|
+
setMessageColor("red");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setStep(3);
|
|
56
|
+
const isDb = dbImages.some(db => imageName.trim().toLowerCase().includes(db));
|
|
57
|
+
if (isDb) {
|
|
58
|
+
setMessage("Warning: This image usually requires environment variables (e.g. MYSQL_ROOT_PASSWORD=my-secret-pw for MySQL, POSTGRES_PASSWORD=yourpassword for Postgres). Enter them as VAR=val,VAR2=val2 or leave empty and press Enter.");
|
|
59
|
+
setMessageColor("yellow");
|
|
60
|
+
} else {
|
|
61
|
+
setMessage("Optional: Enter environment variables (format VAR1=val1,VAR2=val2) or leave empty and press Enter");
|
|
62
|
+
setMessageColor("yellow");
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (step === 3) {
|
|
67
|
+
// Final step: call onCreate with all data
|
|
68
|
+
onCreate({ imageName, containerName, portInput, envInput });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Cancels the creation process and resets state.
|
|
74
|
+
*/
|
|
75
|
+
function cancelCreation() {
|
|
76
|
+
setStep(0);
|
|
77
|
+
setImageName("");
|
|
78
|
+
setContainerName("");
|
|
79
|
+
setPortInput("");
|
|
80
|
+
setEnvInput("");
|
|
81
|
+
setMessage("");
|
|
82
|
+
setMessageColor("yellow");
|
|
83
|
+
if (onCancel) onCancel();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
step,
|
|
88
|
+
setStep,
|
|
89
|
+
imageName,
|
|
90
|
+
setImageName,
|
|
91
|
+
containerName,
|
|
92
|
+
setContainerName,
|
|
93
|
+
portInput,
|
|
94
|
+
setPortInput,
|
|
95
|
+
envInput,
|
|
96
|
+
setEnvInput,
|
|
97
|
+
message,
|
|
98
|
+
setMessage,
|
|
99
|
+
messageColor,
|
|
100
|
+
setMessageColor,
|
|
101
|
+
nextStep,
|
|
102
|
+
cancelCreation,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook to manage the logs viewer state and stream reference.
|
|
5
|
+
* Handles showing/hiding logs and storing log lines.
|
|
6
|
+
*
|
|
7
|
+
* @returns {Object} Logs viewer state and helpers
|
|
8
|
+
* @property {boolean} showLogs - Whether the logs viewer is open
|
|
9
|
+
* @property {function} setShowLogs - Setter for showLogs
|
|
10
|
+
* @property {Array<string>} logs - Array of log lines
|
|
11
|
+
* @property {function} setLogs - Setter for logs
|
|
12
|
+
* @property {object} logsStreamRef - Ref to the logs stream (for cleanup)
|
|
13
|
+
* @property {function} openLogs - Helper to open logs for a container
|
|
14
|
+
* @property {function} closeLogs - Helper to close logs and cleanup
|
|
15
|
+
*/
|
|
16
|
+
export function useLogsViewer() {
|
|
17
|
+
const [showLogs, setShowLogs] = useState(false);
|
|
18
|
+
const [logs, setLogs] = useState([]);
|
|
19
|
+
const logsStreamRef = useRef(null);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Opens the logs viewer and resets logs.
|
|
23
|
+
*/
|
|
24
|
+
function openLogs() {
|
|
25
|
+
setShowLogs(true);
|
|
26
|
+
setLogs([]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Closes the logs viewer and cleans up the stream.
|
|
31
|
+
*/
|
|
32
|
+
function closeLogs() {
|
|
33
|
+
setShowLogs(false);
|
|
34
|
+
setLogs([]);
|
|
35
|
+
if (logsStreamRef.current) {
|
|
36
|
+
logsStreamRef.current.destroy?.();
|
|
37
|
+
logsStreamRef.current = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
showLogs,
|
|
43
|
+
setShowLogs,
|
|
44
|
+
logs,
|
|
45
|
+
setLogs,
|
|
46
|
+
logsStreamRef,
|
|
47
|
+
openLogs,
|
|
48
|
+
closeLogs,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* const [containers, refresh] = useContainers();
|
|
10
10
|
*/
|
|
11
11
|
import React, { useState, useEffect } from "react";
|
|
12
|
-
import { getContainers } from "../helpers/dockerService";
|
|
12
|
+
import { getContainers } from "../helpers/dockerService/serviceComponents/containerList";
|
|
13
13
|
|
|
14
14
|
export function useContainers() {
|
|
15
15
|
const [containers, setContainers] = useState([]);
|