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.
Files changed (68) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +30 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  3. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +14 -0
  4. package/.github/workflows/ci.yml +29 -0
  5. package/CHANGELOG.md +34 -0
  6. package/CODE_OF_CONDUCT.md +12 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.md +83 -166
  9. package/SECURITY.md +5 -0
  10. package/babel.config.cjs +6 -0
  11. package/dist/App.js +38 -24
  12. package/dist/cdd.bundle.js +8 -0
  13. package/dist/components/ContainerCreationPrompt.js +48 -0
  14. package/dist/components/ContainerCreator.js +81 -0
  15. package/dist/components/ContainerList.js +24 -4
  16. package/dist/components/ContainerRow.js +66 -13
  17. package/dist/components/ContainerSection.js +14 -0
  18. package/dist/components/Footer.js +10 -0
  19. package/dist/components/PromptField.js +20 -0
  20. package/dist/components/UsageMenu.js +8 -0
  21. package/dist/helpers/actionHelpers.js +0 -1
  22. package/dist/helpers/dockerActions.js +13 -0
  23. package/dist/helpers/dockerService/dockerService.js +5 -0
  24. package/dist/helpers/dockerService/serviceComponents/containerActions.js +163 -0
  25. package/dist/helpers/dockerService/serviceComponents/containerList.js +57 -0
  26. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +24 -0
  27. package/dist/helpers/dockerService/serviceComponents/containerStats.js +50 -0
  28. package/dist/helpers/dockerService/serviceComponents/imageUtils.js +52 -0
  29. package/dist/helpers/dockerService.js +111 -0
  30. package/dist/helpers/validationHelpers.js +26 -0
  31. package/dist/hooks/creation/useContainerActions.js +171 -0
  32. package/dist/hooks/creation/useContainerCreation.js +141 -0
  33. package/dist/hooks/creation/useLogsViewer.js +62 -0
  34. package/dist/hooks/useContainers.js +11 -1
  35. package/dist/hooks/useControls.js +298 -78
  36. package/dist/hooks/useLogsStream.js +1 -1
  37. package/dist/index.js +11 -0
  38. package/esbuild.config.cjs +11 -0
  39. package/jest.config.cjs +6 -0
  40. package/package.json +10 -6
  41. package/src/App.jsx +32 -26
  42. package/src/components/ContainerCreationPrompt.jsx +37 -0
  43. package/src/components/ContainerList.jsx +10 -6
  44. package/src/components/ContainerRow.jsx +36 -19
  45. package/src/components/ContainerSection.jsx +10 -0
  46. package/src/components/Footer.jsx +10 -0
  47. package/src/components/PromptField.jsx +18 -0
  48. package/src/components/UsageMenu.jsx +17 -0
  49. package/src/helpers/actionHelpers.js +0 -1
  50. package/src/helpers/dockerService/dockerService.js +3 -0
  51. package/src/helpers/dockerService/serviceComponents/containerActions.js +52 -0
  52. package/src/helpers/dockerService/serviceComponents/containerList.js +26 -0
  53. package/src/helpers/dockerService/serviceComponents/containerLogs.js +19 -0
  54. package/src/helpers/dockerService/serviceComponents/containerStats.js +34 -0
  55. package/src/helpers/dockerService/serviceComponents/imageUtils.js +21 -0
  56. package/src/helpers/validationHelpers.js +16 -0
  57. package/src/hooks/creation/useContainerActions.js +68 -0
  58. package/src/hooks/creation/useContainerCreation.js +104 -0
  59. package/src/hooks/creation/useLogsViewer.js +50 -0
  60. package/src/hooks/useContainers.js +1 -1
  61. package/src/hooks/useControls.js +176 -55
  62. package/src/hooks/useLogsStream.js +1 -1
  63. package/test/validationHelpers.test.js +28 -0
  64. package/PLAN_KEYBINDINGS_Y_REALTIME.md +0 -260
  65. package/docs/en/README.md +0 -167
  66. package/src/components/StatsViewer.jsx +0 -0
  67. package/src/helpers/dockerActions.js +0 -52
  68. 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="center">
23
- <Text color={i === selected ? "green" : undefined}>
24
- {i === selected ? "" : " "}
25
- </Text>
26
- <ContainerRow container={container} isSelected={i === selected} />
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 chalk from "chalk";
4
- import { getStats } from "../helpers/dockerService.js";
5
- import StatsBar from "./StatsBar";
3
+ import { getStats } from "../helpers/dockerService/serviceComponents/containerStats";
4
+ import StatsBar from "./StatsBar.jsx";
6
5
 
7
- const colorByState = (state) => {
8
- if (state === "running") return chalk.greenBright("🟢 RUNNING");
9
- if (state === "exited") return chalk.redBright("🔴 EXITED");
10
- if (state === "paused") return chalk.yellowBright("🟠 PAUSED");
11
- return chalk.gray(state.toUpperCase());
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
- const s = await getStats(id);
26
- setStats(s);
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
- <Text>
36
- {chalk.cyan(name.padEnd(20))} {chalk.gray(image.padEnd(20))}{" "}
37
- {state === "running"
38
- ? chalk.greenBright("🟢 RUNNING")
39
- : chalk.redBright(`🔴 ${state.toUpperCase()}`)}
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
- </Text>
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,10 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export default function Footer() {
5
+ return (
6
+ <Box justifyContent="flex-end" width="100%">
7
+ <Text dimColor>Crafted by Carlos Cochero • 2025</Text>
8
+ </Box>
9
+ );
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
+ }
@@ -1,4 +1,3 @@
1
- // Helper para manejar acciones docker con feedback visual y validación
2
1
  export async function handleAction({
3
2
  containers,
4
3
  selected,
@@ -0,0 +1,3 @@
1
+ import Docker from "dockerode";
2
+ const docker = new Docker({ socketPath: "/var/run/docker.sock" });
3
+ export { docker };
@@ -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([]);