cdd-cli 3.2.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.~lock.ROADMAP.md# +1 -0
- package/CHANGELOG.md +51 -0
- package/README.es.md +198 -0
- package/README.md +81 -23
- package/ROADMAP.md +647 -0
- package/coverage/clover.xml +400 -519
- package/coverage/coverage-final.json +16 -20
- package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +400 -0
- package/coverage/lcov-report/ControlsHUD.jsx.html +256 -0
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +31 -13
- package/coverage/lcov-report/components/PromptField.jsx.html +4 -4
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +37 -13
- package/coverage/lcov-report/components/index.html +14 -14
- package/coverage/lcov-report/helpers/constants.js.html +87 -33
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +40 -40
- package/coverage/lcov-report/helpers/dockerHubService.js.html +310 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +1 -1
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +1 -1
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +1 -1
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +18 -18
- package/coverage/lcov-report/helpers/index.html +77 -62
- package/coverage/lcov-report/helpers/logger.js.html +60 -60
- package/coverage/lcov-report/helpers/safeCall.js.html +14 -14
- package/coverage/lcov-report/helpers/validationHelpers.js.html +52 -52
- package/coverage/lcov-report/hooks/creation/index.html +19 -19
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +4 -4
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +225 -48
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +5 -5
- package/coverage/lcov-report/hooks/debug/index.html +1 -1
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +7 -7
- package/coverage/lcov-report/hooks/index.html +15 -15
- package/coverage/lcov-report/hooks/navigation/index.html +1 -1
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +6 -6
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +13 -13
- package/coverage/lcov-report/hooks/useControls.js.html +82 -40
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +5 -5
- package/coverage/lcov-report/hooks/useExitHandler.js.html +6 -6
- package/coverage/lcov-report/index.html +38 -68
- package/coverage/lcov.info +742 -981
- package/dist/App.js +7 -1
- package/dist/components/ContainerCreationPrompt.js +26 -10
- package/dist/components/ControlsHUD.js +106 -0
- package/dist/components/SuggestionPanel.js +16 -7
- package/dist/helpers/constants.js +60 -21
- package/dist/helpers/dockerHubService.js +111 -0
- package/dist/helpers/logger.js +1 -1
- package/dist/hooks/creation/useContainerActions.js +22 -5
- package/dist/hooks/creation/useContainerCreation.js +395 -107
- package/dist/hooks/useContainerCommandRouter.js +1 -1
- package/dist/hooks/useControls.js +23 -7
- package/dist/index.js +7 -0
- package/package.json +1 -1
- package/src/App.jsx +6 -0
- package/src/components/ContainerCreationPrompt.jsx +15 -5
- package/src/components/ControlsHUD.jsx +66 -0
- package/src/components/SuggestionPanel.jsx +10 -2
- package/src/helpers/constants.js +38 -20
- package/src/helpers/dockerHubService.js +75 -0
- package/src/helpers/logger.js +1 -2
- package/src/hooks/creation/useContainerActions.js +19 -6
- package/src/hooks/creation/useContainerCreation.js +224 -87
- package/src/hooks/useContainerCommandRouter.js +1 -1
- package/src/hooks/useControls.js +24 -8
- package/src/index.js +4 -0
- package/test/ContainerCreationPrompt.dom.test.js +245 -0
- package/test/ControlsHUD.dom.test.js +125 -0
- package/test/SuggestionPanel.dom.test.js +30 -0
- package/test/constants.test.js +7 -0
- package/test/dockerHubService.test.js +213 -0
- package/test/logger.test.js +58 -0
- package/test/resolveImageTag.test.js +26 -0
- package/test/useContainerCreation.dom.test.js +306 -3
- package/test/useContainerCreationHub.dom.test.js +184 -0
- package/test/useControls.dom.test.js +212 -6
|
@@ -36,7 +36,9 @@ import { DB_IMAGES } from '../helpers/constants.js';
|
|
|
36
36
|
* @returns {Object} controls - API for the App component
|
|
37
37
|
*/
|
|
38
38
|
export function useControls() {
|
|
39
|
+
var _overrides$triggerHub, _overrides$isSearchin;
|
|
39
40
|
var containers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
41
|
+
var overrides = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
40
42
|
var _React$useState = React.useState(false),
|
|
41
43
|
_React$useState2 = _slicedToArray(_React$useState, 2),
|
|
42
44
|
creatingContainer = _React$useState2[0],
|
|
@@ -60,8 +62,7 @@ export function useControls() {
|
|
|
60
62
|
portInput: portInput,
|
|
61
63
|
envInput: envInput
|
|
62
64
|
});
|
|
63
|
-
actions.
|
|
64
|
-
actions.setMessageColor('yellow');
|
|
65
|
+
actions.setTimedMessage("Creating container ".concat(imageName, "..."), 'yellow');
|
|
65
66
|
_context.p = 1;
|
|
66
67
|
_context.n = 2;
|
|
67
68
|
return svcCreateContainer(imageName, options);
|
|
@@ -75,15 +76,13 @@ export function useControls() {
|
|
|
75
76
|
return "".concat(p.hostPort, "\u2192").concat(p.containerPort, "/").concat(p.protocol);
|
|
76
77
|
}).join(', ');
|
|
77
78
|
}
|
|
78
|
-
actions.
|
|
79
|
-
actions.setMessageColor('green');
|
|
79
|
+
actions.setTimedMessage("Created container ".concat(id).concat(portMsg), 'green');
|
|
80
80
|
_context.n = 4;
|
|
81
81
|
break;
|
|
82
82
|
case 3:
|
|
83
83
|
_context.p = 3;
|
|
84
84
|
_t = _context.v;
|
|
85
|
-
actions.
|
|
86
|
-
actions.setMessageColor('red');
|
|
85
|
+
actions.setTimedMessage("Error creating container: ".concat(_t.message), 'red');
|
|
87
86
|
case 4:
|
|
88
87
|
_context.p = 4;
|
|
89
88
|
setCreatingContainer(false);
|
|
@@ -106,6 +105,11 @@ export function useControls() {
|
|
|
106
105
|
var logsViewer = useLogsViewer();
|
|
107
106
|
var selection = useContainerSelection(containers.length);
|
|
108
107
|
var debugLogs = useDebugLogs();
|
|
108
|
+
|
|
109
|
+
// Allow overrides for testability (e.g. injecting a mock triggerHubSearch)
|
|
110
|
+
var triggerHubSearch = (_overrides$triggerHub = overrides.triggerHubSearch) !== null && _overrides$triggerHub !== void 0 ? _overrides$triggerHub : creation.triggerHubSearch;
|
|
111
|
+
var isSearchingHub = (_overrides$isSearchin = overrides.isSearchingHub) !== null && _overrides$isSearchin !== void 0 ? _overrides$isSearchin : creation.isSearchingHub;
|
|
112
|
+
var insertNextSuggestedEnv = overrides.insertNextSuggestedEnv !== undefined ? overrides.insertNextSuggestedEnv : creation.insertNextSuggestedEnv;
|
|
109
113
|
var eraseConfirmation = useEraseConfirmation({
|
|
110
114
|
onConfirm: function onConfirm() {
|
|
111
115
|
actions.handleAction({
|
|
@@ -211,6 +215,18 @@ export function useControls() {
|
|
|
211
215
|
return;
|
|
212
216
|
}
|
|
213
217
|
|
|
218
|
+
// Tab on step 0: trigger Docker Hub search (with guards)
|
|
219
|
+
if (key.tab) {
|
|
220
|
+
if (step === 0) {
|
|
221
|
+
if (!isSearchingHub && (creation.imageName || '').trim() !== '') {
|
|
222
|
+
triggerHubSearch();
|
|
223
|
+
}
|
|
224
|
+
} else if (step === 3) {
|
|
225
|
+
insertNextSuggestedEnv === null || insertNextSuggestedEnv === void 0 || insertNextSuggestedEnv();
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
214
230
|
// Arrow keys on step 0: navigate suggestion list
|
|
215
231
|
if (step === 0 && creation.suggestions.length > 0) {
|
|
216
232
|
if (key.upArrow) {
|
|
@@ -240,7 +256,7 @@ export function useControls() {
|
|
|
240
256
|
if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
|
|
241
257
|
if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
|
|
242
258
|
}
|
|
243
|
-
}, [creation, setCreatingContainer]);
|
|
259
|
+
}, [creation, setCreatingContainer, triggerHubSearch, isSearchingHub, insertNextSuggestedEnv]);
|
|
244
260
|
|
|
245
261
|
// Single keyboard entry point
|
|
246
262
|
useInput(function (input, key) {
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,12 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { render } from 'ink';
|
|
12
12
|
import App from './App.js';
|
|
13
|
+
import { logger } from './helpers/logger.js';
|
|
14
|
+
process.on('uncaughtException', function (err) {
|
|
15
|
+
return logger.error('Uncaught exception', err);
|
|
16
|
+
});
|
|
17
|
+
process.on('unhandledRejection', function (reason) {
|
|
18
|
+
return logger.error('Unhandled rejection', reason);
|
|
19
|
+
});
|
|
13
20
|
console.clear();
|
|
14
21
|
render(/*#__PURE__*/React.createElement(App, null));
|
package/package.json
CHANGED
package/src/App.jsx
CHANGED
|
@@ -35,6 +35,12 @@ export default function App() {
|
|
|
35
35
|
envInput={controls.envInput}
|
|
36
36
|
message={controls.message}
|
|
37
37
|
messageColor={controls.messageColor}
|
|
38
|
+
suggestions={controls.creation.suggestions}
|
|
39
|
+
selectedSuggestionIndex={controls.creation.selectedSuggestionIndex}
|
|
40
|
+
visibleOffset={controls.creation.visibleOffset}
|
|
41
|
+
isSearchingHub={controls.creation.isSearchingHub}
|
|
42
|
+
hubResults={controls.creation.hubResults}
|
|
43
|
+
hasSuggestedEnv={controls.creation.hasSuggestedEnv}
|
|
38
44
|
/>
|
|
39
45
|
);
|
|
40
46
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from 'ink';
|
|
3
3
|
import { PromptField, PromptMessage } from './PromptField.jsx';
|
|
4
4
|
import { SuggestionPanel } from './SuggestionPanel.jsx';
|
|
5
|
+
import { ControlsHUD } from './ControlsHUD.jsx';
|
|
5
6
|
import PropTypes from 'prop-types';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -34,10 +35,13 @@ export default function ContainerCreationPrompt(props) {
|
|
|
34
35
|
suggestions = [],
|
|
35
36
|
selectedSuggestionIndex = -1,
|
|
36
37
|
visibleOffset = 0,
|
|
38
|
+
hubResults = null,
|
|
39
|
+
isSearchingHub = false,
|
|
40
|
+
hasSuggestedEnv = false,
|
|
37
41
|
} = props;
|
|
38
42
|
const prompts = [
|
|
39
43
|
{
|
|
40
|
-
label: 'Name of the image to create (e.g., nginx:
|
|
44
|
+
label: 'Name of the image to create (e.g., nginx:1.27-alpine):',
|
|
41
45
|
value: imageName,
|
|
42
46
|
required: true,
|
|
43
47
|
},
|
|
@@ -58,7 +62,9 @@ export default function ContainerCreationPrompt(props) {
|
|
|
58
62
|
},
|
|
59
63
|
];
|
|
60
64
|
const { label, value, required } = prompts[step] || {};
|
|
61
|
-
const
|
|
65
|
+
const activeItems = hubResults ?? suggestions;
|
|
66
|
+
const showSuggestions = step === 0 && (isSearchingHub || activeItems.length > 0);
|
|
67
|
+
const hasSuggestions = (suggestions?.length > 0) || (hubResults?.length > 0);
|
|
62
68
|
return (
|
|
63
69
|
<Box
|
|
64
70
|
flexDirection="column"
|
|
@@ -69,13 +75,14 @@ export default function ContainerCreationPrompt(props) {
|
|
|
69
75
|
<PromptField label={label} value={value} required={required} />
|
|
70
76
|
{showSuggestions && (
|
|
71
77
|
<SuggestionPanel
|
|
72
|
-
items={
|
|
78
|
+
items={activeItems}
|
|
73
79
|
selectedIndex={selectedSuggestionIndex}
|
|
74
80
|
visibleOffset={visibleOffset}
|
|
81
|
+
isLoading={isSearchingHub}
|
|
75
82
|
/>
|
|
76
83
|
)}
|
|
77
84
|
<PromptMessage message={message} color={messageColor} />
|
|
78
|
-
<
|
|
85
|
+
<ControlsHUD step={step} hasSuggestions={hasSuggestions} isSearchingHub={isSearchingHub} hasSuggestedEnv={hasSuggestedEnv} />
|
|
79
86
|
</Box>
|
|
80
87
|
);
|
|
81
88
|
}
|
|
@@ -91,6 +98,9 @@ ContainerCreationPrompt.propTypes = {
|
|
|
91
98
|
suggestions: PropTypes.arrayOf(PropTypes.string),
|
|
92
99
|
selectedSuggestionIndex: PropTypes.number,
|
|
93
100
|
visibleOffset: PropTypes.number,
|
|
101
|
+
hubResults: PropTypes.arrayOf(PropTypes.string),
|
|
102
|
+
isSearchingHub: PropTypes.bool,
|
|
103
|
+
hasSuggestedEnv: PropTypes.bool,
|
|
94
104
|
};
|
|
95
105
|
|
|
96
106
|
// Named export for test compatibility with jest ESM interop
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the contextual keyboard hints for the current wizard state.
|
|
6
|
+
*
|
|
7
|
+
* @param {number} step - Current wizard step (0-3)
|
|
8
|
+
* @param {boolean} hasSuggestions - Whether suggestions/hub results are visible
|
|
9
|
+
* @param {boolean} isSearchingHub - Whether a Hub search is in progress
|
|
10
|
+
* @param {boolean} [hasSuggestedEnv=false] - Whether the image has suggestedEnv entries
|
|
11
|
+
* @returns {{ key: string, label: string }[]}
|
|
12
|
+
*/
|
|
13
|
+
function getHints(step, hasSuggestions, isSearchingHub, hasSuggestedEnv = false) {
|
|
14
|
+
if (step === 0) {
|
|
15
|
+
if (hasSuggestions) {
|
|
16
|
+
return [
|
|
17
|
+
{ key: '↑↓', label: 'Navigate' },
|
|
18
|
+
{ key: 'Enter', label: 'Select' },
|
|
19
|
+
{ key: 'Esc', label: 'Cancel' },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
const hints = [];
|
|
23
|
+
if (!isSearchingHub) hints.push({ key: 'Tab', label: 'Search Hub' });
|
|
24
|
+
hints.push({ key: '↑↓', label: 'Browse' });
|
|
25
|
+
hints.push({ key: 'Enter', label: 'Confirm' });
|
|
26
|
+
hints.push({ key: 'Esc', label: 'Cancel' });
|
|
27
|
+
return hints;
|
|
28
|
+
}
|
|
29
|
+
if (step === 3) {
|
|
30
|
+
const hints = [];
|
|
31
|
+
if (hasSuggestedEnv) hints.push({ key: 'Tab', label: 'Insert next env' });
|
|
32
|
+
hints.push({ key: 'Enter', label: 'Continue' });
|
|
33
|
+
hints.push({ key: 'Esc', label: 'Cancel' });
|
|
34
|
+
return hints;
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
{ key: 'Enter', label: 'Continue' },
|
|
38
|
+
{ key: 'Esc', label: 'Cancel' },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Displays a row of contextual keyboard shortcut hints for the active wizard step.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} props
|
|
46
|
+
* @param {number} props.step - Current wizard step
|
|
47
|
+
* @param {boolean} [props.hasSuggestions=false] - Whether suggestions/hub results are visible
|
|
48
|
+
* @param {boolean} [props.isSearchingHub=false] - Whether a Hub search is in progress
|
|
49
|
+
* @param {boolean} [props.hasSuggestedEnv=false] - Whether the image has suggestedEnv entries
|
|
50
|
+
*/
|
|
51
|
+
function ControlsHUD({ step, hasSuggestions = false, isSearchingHub = false, hasSuggestedEnv = false }) {
|
|
52
|
+
const hints = getHints(step, hasSuggestions, isSearchingHub, hasSuggestedEnv);
|
|
53
|
+
return (
|
|
54
|
+
<Box flexDirection="row" columnGap={2}>
|
|
55
|
+
{hints.map(({ key, label }) => (
|
|
56
|
+
<Box key={key}>
|
|
57
|
+
<Text color="cyan">[{key}]</Text>
|
|
58
|
+
<Text dimColor> {label}</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
))}
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default ControlsHUD;
|
|
66
|
+
export { getHints, ControlsHUD };
|
|
@@ -19,10 +19,12 @@ export function SuggestionPanel({
|
|
|
19
19
|
selectedIndex,
|
|
20
20
|
visibleOffset,
|
|
21
21
|
maxVisible = 6,
|
|
22
|
+
isLoading = false,
|
|
22
23
|
}) {
|
|
23
|
-
|
|
24
|
+
const hasItems = items && items.length > 0;
|
|
25
|
+
if (!hasItems && !isLoading) return null;
|
|
24
26
|
|
|
25
|
-
const visible = items.slice(visibleOffset, visibleOffset + maxVisible);
|
|
27
|
+
const visible = hasItems ? items.slice(visibleOffset, visibleOffset + maxVisible) : [];
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<Box
|
|
@@ -41,6 +43,11 @@ export function SuggestionPanel({
|
|
|
41
43
|
</Text>
|
|
42
44
|
);
|
|
43
45
|
})}
|
|
46
|
+
{isLoading && (
|
|
47
|
+
<Text dimColor color="gray">
|
|
48
|
+
{' [searching Docker Hub...]'}
|
|
49
|
+
</Text>
|
|
50
|
+
)}
|
|
44
51
|
</Box>
|
|
45
52
|
);
|
|
46
53
|
}
|
|
@@ -50,4 +57,5 @@ SuggestionPanel.propTypes = {
|
|
|
50
57
|
selectedIndex: PropTypes.number.isRequired,
|
|
51
58
|
visibleOffset: PropTypes.number.isRequired,
|
|
52
59
|
maxVisible: PropTypes.number,
|
|
60
|
+
isLoading: PropTypes.bool,
|
|
53
61
|
};
|
package/src/helpers/constants.js
CHANGED
|
@@ -48,24 +48,42 @@ export const DB_IMAGES = [
|
|
|
48
48
|
* @type {Record<string, { requiredEnv: string[], defaultPort: string, suggestedEnv: string[] }>}
|
|
49
49
|
*/
|
|
50
50
|
export const IMAGE_PROFILES = {
|
|
51
|
-
nginx: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
|
|
52
|
-
httpd: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
|
|
53
|
-
caddy: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
|
|
54
|
-
node: { requiredEnv: [], defaultPort: '3000', suggestedEnv: ['NODE_ENV=production'] },
|
|
55
|
-
python: { requiredEnv: [], defaultPort: '8000', suggestedEnv: ['PYTHONUNBUFFERED=1'] },
|
|
56
|
-
openjdk: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m'] },
|
|
57
|
-
golang: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['GOMAXPROCS=2'] },
|
|
58
|
-
redis: { requiredEnv: [], defaultPort: '6379', suggestedEnv: ['REDIS_PASSWORD=change-me'] },
|
|
59
|
-
memcached: { requiredEnv: [], defaultPort: '11211', suggestedEnv: [] },
|
|
60
|
-
postgres: { requiredEnv: ['POSTGRES_PASSWORD'], defaultPort: '5432', suggestedEnv: ['POSTGRES_USER=postgres', 'POSTGRES_DB=app'] },
|
|
61
|
-
mysql: { requiredEnv: ['MYSQL_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123'] },
|
|
62
|
-
mariadb: { requiredEnv: ['MARIADB_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123'] },
|
|
63
|
-
mongo: { requiredEnv: [], defaultPort: '27017', suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret'] },
|
|
64
|
-
mssql: { requiredEnv: ['ACCEPT_EULA', 'SA_PASSWORD'], defaultPort: '1433', suggestedEnv: ['ACCEPT_EULA=Y', 'MSSQL_PID=Developer'] },
|
|
65
|
-
rabbitmq: { requiredEnv: [], defaultPort: '5672', suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest'] },
|
|
66
|
-
kafka: { requiredEnv: [], defaultPort: '9092', suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092', 'KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092'] },
|
|
67
|
-
zookeeper: { requiredEnv: [], defaultPort: '2181', suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes'] },
|
|
68
|
-
elasticsearch: { requiredEnv: [], defaultPort: '9200', suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false'] },
|
|
69
|
-
minio: { requiredEnv: ['MINIO_ROOT_USER', 'MINIO_ROOT_PASSWORD'], defaultPort: '9000', suggestedEnv: ['MINIO_CONSOLE_ADDRESS=:9001'] },
|
|
70
|
-
wordpress: { requiredEnv: ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'], defaultPort: '8080', suggestedEnv: ['WORDPRESS_TABLE_PREFIX=wp_'] },
|
|
51
|
+
nginx: { requiredEnv: [], defaultPort: '80', suggestedEnv: [], defaultTag: '1.27-alpine' },
|
|
52
|
+
httpd: { requiredEnv: [], defaultPort: '80', suggestedEnv: [], defaultTag: '2.4-alpine' },
|
|
53
|
+
caddy: { requiredEnv: [], defaultPort: '80', suggestedEnv: [], defaultTag: '2-alpine' },
|
|
54
|
+
node: { requiredEnv: [], defaultPort: '3000', suggestedEnv: ['NODE_ENV=production'], defaultTag: '22-alpine' },
|
|
55
|
+
python: { requiredEnv: [], defaultPort: '8000', suggestedEnv: ['PYTHONUNBUFFERED=1'], defaultTag: '3.12-slim' },
|
|
56
|
+
openjdk: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m'], defaultTag: '21-slim' },
|
|
57
|
+
golang: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['GOMAXPROCS=2'], defaultTag: '1.22-alpine' },
|
|
58
|
+
redis: { requiredEnv: [], defaultPort: '6379', suggestedEnv: ['REDIS_PASSWORD=change-me'], defaultTag: '7-alpine' },
|
|
59
|
+
memcached: { requiredEnv: [], defaultPort: '11211', suggestedEnv: [], defaultTag: '1.6-alpine' },
|
|
60
|
+
postgres: { requiredEnv: ['POSTGRES_PASSWORD'], defaultPort: '5432', suggestedEnv: ['POSTGRES_PASSWORD=secret', 'POSTGRES_USER=postgres', 'POSTGRES_DB=app'], defaultTag: '17-alpine' },
|
|
61
|
+
mysql: { requiredEnv: ['MYSQL_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MYSQL_ROOT_PASSWORD=secret', 'MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123'], defaultTag: '8.0' },
|
|
62
|
+
mariadb: { requiredEnv: ['MARIADB_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MARIADB_ROOT_PASSWORD=secret', 'MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123'], defaultTag: '11-alpine' },
|
|
63
|
+
mongo: { requiredEnv: [], defaultPort: '27017', suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret'], defaultTag: '7.0' },
|
|
64
|
+
mssql: { requiredEnv: ['ACCEPT_EULA', 'SA_PASSWORD'], defaultPort: '1433', suggestedEnv: ['ACCEPT_EULA=Y', 'SA_PASSWORD=Strong!Passw0rd', 'MSSQL_PID=Developer'], defaultTag: '2022-latest' },
|
|
65
|
+
rabbitmq: { requiredEnv: [], defaultPort: '5672', suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest'], defaultTag: '3-management-alpine' },
|
|
66
|
+
kafka: { requiredEnv: [], defaultPort: '9092', suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092', 'KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092'], defaultTag: '3.7' },
|
|
67
|
+
zookeeper: { requiredEnv: [], defaultPort: '2181', suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes'], defaultTag: '3.9' },
|
|
68
|
+
elasticsearch: { requiredEnv: [], defaultPort: '9200', suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false'], defaultTag: '8.13.0' },
|
|
69
|
+
minio: { requiredEnv: ['MINIO_ROOT_USER', 'MINIO_ROOT_PASSWORD'], defaultPort: '9000', suggestedEnv: ['MINIO_ROOT_USER=admin', 'MINIO_ROOT_PASSWORD=secret', 'MINIO_CONSOLE_ADDRESS=:9001'], defaultTag: 'latest' },
|
|
70
|
+
wordpress: { requiredEnv: ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'], defaultPort: '8080', suggestedEnv: ['WORDPRESS_DB_HOST=db', 'WORDPRESS_DB_USER=wp', 'WORDPRESS_DB_PASSWORD=secret', 'WORDPRESS_DB_NAME=wordpress', 'WORDPRESS_TABLE_PREFIX=wp_'], defaultTag: '6.5-apache' },
|
|
71
71
|
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Given an image name (with or without tag), returns the image name with a
|
|
75
|
+
* default tag appended when the image matches a known profile and has no
|
|
76
|
+
* explicit tag. Returns the input unchanged otherwise.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} imageName - e.g. 'postgres', 'postgres:15', 'unknownimage'
|
|
79
|
+
* @param {Record<string, { defaultTag: string }>} profiles
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function resolveImageTag(imageName, profiles = IMAGE_PROFILES) {
|
|
83
|
+
if (!imageName) return imageName;
|
|
84
|
+
if (imageName.includes(':')) return imageName;
|
|
85
|
+
const baseName = imageName.trim().toLowerCase().split('/').pop();
|
|
86
|
+
const profile = profiles[baseName];
|
|
87
|
+
if (!profile?.defaultTag) return imageName;
|
|
88
|
+
return `${imageName}:${profile.defaultTag}`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Hub search helper.
|
|
3
|
+
* Pure function — no side effects beyond HTTP. All state management
|
|
4
|
+
* lives in the calling hook (useContainerCreation).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DOCKER_HUB_SEARCH_URL =
|
|
8
|
+
'https://hub.docker.com/v2/search/repositories/';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format a Docker Hub search result object into a display string.
|
|
12
|
+
*
|
|
13
|
+
* Format rules:
|
|
14
|
+
* - Official: "name [★ official] [Npulls+]" (if pullCount > 0)
|
|
15
|
+
* - Non-official with pulls: "name [Npulls+]"
|
|
16
|
+
* - Otherwise: "name"
|
|
17
|
+
*
|
|
18
|
+
* Pull count suffixes: ≥1B → "1B+", ≥1M → "1M+", ≥1K → "1K+", else raw number
|
|
19
|
+
*
|
|
20
|
+
* @param {{ name: string, isOfficial: boolean, pullCount: number }} result
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function formatHubResult(result) {
|
|
24
|
+
const { name, isOfficial, pullCount } = result;
|
|
25
|
+
|
|
26
|
+
let pullSuffix = '';
|
|
27
|
+
if (pullCount >= 1e9) {
|
|
28
|
+
pullSuffix = ` [1B+ pulls]`;
|
|
29
|
+
} else if (pullCount >= 1e6) {
|
|
30
|
+
pullSuffix = ` [${Math.floor(pullCount / 1e6)}M+ pulls]`;
|
|
31
|
+
} else if (pullCount >= 1e3) {
|
|
32
|
+
pullSuffix = ` [${Math.floor(pullCount / 1e3)}K+ pulls]`;
|
|
33
|
+
} else if (pullCount > 0) {
|
|
34
|
+
pullSuffix = ` [${pullCount} pulls]`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isOfficial) {
|
|
38
|
+
return `${name} [★ official]${pullSuffix}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return `${name}${pullSuffix}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Search Docker Hub for images matching the given query.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} query - Search term (will be trimmed and encoded)
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {AbortSignal} [options.signal] - Abort signal; defaults to AbortSignal.timeout(5000)
|
|
50
|
+
* @returns {Promise<Array<{ name: string, isOfficial: boolean, pullCount: number, description: string }>>}
|
|
51
|
+
* @throws {Error} If response.ok is false or the network call rejects
|
|
52
|
+
*/
|
|
53
|
+
export async function searchDockerHub(query, options = {}) {
|
|
54
|
+
const q = query.trim();
|
|
55
|
+
if (!q) return [];
|
|
56
|
+
|
|
57
|
+
const url = `${DOCKER_HUB_SEARCH_URL}?query=${encodeURIComponent(q)}&page_size=10`;
|
|
58
|
+
const signal = options.signal ?? AbortSignal.timeout(5000);
|
|
59
|
+
|
|
60
|
+
const response = await fetch(url, { signal });
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Docker Hub search failed: ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
const results = data.results ?? [];
|
|
68
|
+
|
|
69
|
+
return results.map((item) => ({
|
|
70
|
+
name: item.repo_name,
|
|
71
|
+
isOfficial: item.is_official,
|
|
72
|
+
pullCount: item.pull_count,
|
|
73
|
+
description: item.short_description,
|
|
74
|
+
}));
|
|
75
|
+
}
|
package/src/helpers/logger.js
CHANGED
|
@@ -58,8 +58,7 @@ function log(level, message, ...args) {
|
|
|
58
58
|
const formatted = `[${timestamp}] [${upperLevel}] ${message}`;
|
|
59
59
|
const entry = { level, message, args, timestamp, formatted };
|
|
60
60
|
|
|
61
|
-
const shouldWriteToConsole =
|
|
62
|
-
listeners.size === 0 || level === 'error' || level === 'warn';
|
|
61
|
+
const shouldWriteToConsole = listeners.size === 0;
|
|
63
62
|
|
|
64
63
|
if (shouldWriteToConsole) {
|
|
65
64
|
const method = level === 'debug' ? 'log' : level;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
2
|
import { safeCall } from '../../helpers/safeCall.js';
|
|
3
3
|
import {
|
|
4
4
|
startContainer as svcStartContainer,
|
|
@@ -20,6 +20,19 @@ export function useContainerActions({ containers, onAction }) {
|
|
|
20
20
|
const [message, setMessage] = useState('');
|
|
21
21
|
const [messageColor, setMessageColor] = useState('yellow');
|
|
22
22
|
|
|
23
|
+
const messageTimerRef = useRef(null);
|
|
24
|
+
|
|
25
|
+
function setTimedMessage(msg, color = 'yellow', ms = 4000) {
|
|
26
|
+
clearTimeout(messageTimerRef.current);
|
|
27
|
+
setMessage(msg);
|
|
28
|
+
if (color !== undefined) setMessageColor(color);
|
|
29
|
+
if (msg) {
|
|
30
|
+
messageTimerRef.current = setTimeout(() => setMessage(''), ms);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
useEffect(() => () => clearTimeout(messageTimerRef.current), []);
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* Handles a generic container action and sets feedback.
|
|
25
38
|
* @param {Function} actionFn - The async action function (start, stop, etc)
|
|
@@ -42,20 +55,20 @@ export function useContainerActions({ containers, onAction }) {
|
|
|
42
55
|
setMessageColor('yellow');
|
|
43
56
|
try {
|
|
44
57
|
await actionFn(container.id);
|
|
45
|
-
|
|
46
|
-
setMessageColor('green');
|
|
58
|
+
setTimedMessage(`${actionLabel} container successful.`, 'green');
|
|
47
59
|
safeCall(onAction);
|
|
48
60
|
} catch (err) {
|
|
49
|
-
|
|
50
|
-
`Failed to ${actionLabel.toLowerCase()} container: ${err.message}
|
|
61
|
+
setTimedMessage(
|
|
62
|
+
`Failed to ${actionLabel.toLowerCase()} container: ${err.message}`,
|
|
63
|
+
'red'
|
|
51
64
|
);
|
|
52
|
-
setMessageColor('red');
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
return {
|
|
57
69
|
message,
|
|
58
70
|
setMessage,
|
|
71
|
+
setTimedMessage,
|
|
59
72
|
messageColor,
|
|
60
73
|
setMessageColor,
|
|
61
74
|
handleAction,
|