foreman_openbolt 0.0.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.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +46 -0
- data/Rakefile +106 -0
- data/app/controllers/foreman_openbolt/task_controller.rb +298 -0
- data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +40 -0
- data/app/lib/actions/foreman_openbolt/poll_task_status.rb +151 -0
- data/app/models/foreman_openbolt/task_job.rb +110 -0
- data/app/views/foreman_openbolt/react_page.html.erb +1 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20250819000000_create_openbolt_task_jobs.rb +25 -0
- data/db/migrate/20250925000000_add_command_to_openbolt_task_jobs.rb +7 -0
- data/db/migrate/20251001000000_add_task_description_to_task_jobs.rb +7 -0
- data/db/seeds.d/001_add_openbolt_feature.rb +4 -0
- data/lib/foreman_openbolt/engine.rb +169 -0
- data/lib/foreman_openbolt/version.rb +5 -0
- data/lib/foreman_openbolt.rb +7 -0
- data/lib/proxy_api/openbolt.rb +53 -0
- data/lib/tasks/foreman_openbolt_tasks.rake +48 -0
- data/locale/Makefile +73 -0
- data/locale/en/foreman_openbolt.po +19 -0
- data/locale/foreman_openbolt.pot +19 -0
- data/locale/gemspec.rb +7 -0
- data/package.json +41 -0
- data/test/factories/foreman_openbolt_factories.rb +7 -0
- data/test/test_plugin_helper.rb +8 -0
- data/test/unit/foreman_openbolt_test.rb +13 -0
- data/webpack/global_index.js +4 -0
- data/webpack/global_test_setup.js +11 -0
- data/webpack/index.js +19 -0
- data/webpack/src/Components/LaunchTask/EmptyContent.js +24 -0
- data/webpack/src/Components/LaunchTask/FieldTable.js +147 -0
- data/webpack/src/Components/LaunchTask/HostSelector/HostSearch.js +29 -0
- data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +208 -0
- data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +113 -0
- data/webpack/src/Components/LaunchTask/HostSelector/hostgroups.gql +9 -0
- data/webpack/src/Components/LaunchTask/HostSelector/hosts.gql +10 -0
- data/webpack/src/Components/LaunchTask/HostSelector/index.js +261 -0
- data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +116 -0
- data/webpack/src/Components/LaunchTask/ParameterField.js +145 -0
- data/webpack/src/Components/LaunchTask/ParametersSection.js +66 -0
- data/webpack/src/Components/LaunchTask/SmartProxySelect.js +51 -0
- data/webpack/src/Components/LaunchTask/TaskSelect.js +84 -0
- data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +63 -0
- data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +48 -0
- data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +64 -0
- data/webpack/src/Components/LaunchTask/index.js +333 -0
- data/webpack/src/Components/TaskExecution/ExecutionDetails.js +188 -0
- data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +99 -0
- data/webpack/src/Components/TaskExecution/LoadingIndicator.js +51 -0
- data/webpack/src/Components/TaskExecution/ResultDisplay.js +174 -0
- data/webpack/src/Components/TaskExecution/TaskDetails.js +99 -0
- data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +142 -0
- data/webpack/src/Components/TaskExecution/index.js +130 -0
- data/webpack/src/Components/TaskHistory/TaskPopover.js +95 -0
- data/webpack/src/Components/TaskHistory/index.js +199 -0
- data/webpack/src/Components/common/HostsPopover.js +49 -0
- data/webpack/src/Components/common/constants.js +44 -0
- data/webpack/src/Components/common/helpers.js +19 -0
- data/webpack/src/Pages/LaunchTaskPage.js +12 -0
- data/webpack/src/Pages/TaskExecutionPage.js +12 -0
- data/webpack/src/Pages/TaskHistoryPage.js +12 -0
- data/webpack/src/Router/routes.js +30 -0
- data/webpack/test_setup.js +17 -0
- data/webpack/webpack.config.js +7 -0
- metadata +208 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Flex,
|
|
7
|
+
FlexItem,
|
|
8
|
+
FormGroup,
|
|
9
|
+
FormSelect,
|
|
10
|
+
FormSelectOption,
|
|
11
|
+
Spinner,
|
|
12
|
+
} from '@patternfly/react-core';
|
|
13
|
+
import { SyncIcon } from '@patternfly/react-icons';
|
|
14
|
+
|
|
15
|
+
const TaskSelect = ({
|
|
16
|
+
taskNames,
|
|
17
|
+
selectedTask,
|
|
18
|
+
onTaskChange,
|
|
19
|
+
onReloadTasks,
|
|
20
|
+
isLoading,
|
|
21
|
+
isDisabled,
|
|
22
|
+
}) => (
|
|
23
|
+
<FormGroup label={__('Task Name')} fieldId="task-name-input">
|
|
24
|
+
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
|
|
25
|
+
<FlexItem flex={{ default: 'flex_1' }}>
|
|
26
|
+
<FormSelect
|
|
27
|
+
id="task-select"
|
|
28
|
+
// Force remount on isDisabled so the tooltip based on the title changes
|
|
29
|
+
key={`task-select-${isDisabled}`}
|
|
30
|
+
title={
|
|
31
|
+
isDisabled
|
|
32
|
+
? __('You must first select a Smart Proxy')
|
|
33
|
+
: __('Select a task to execute.')
|
|
34
|
+
}
|
|
35
|
+
value={selectedTask}
|
|
36
|
+
onChange={onTaskChange}
|
|
37
|
+
isDisabled={isDisabled || isLoading}
|
|
38
|
+
className="without_select2"
|
|
39
|
+
aria-label={__('Select Task')}
|
|
40
|
+
aria-required="true"
|
|
41
|
+
aria-describedby="task-select-helper"
|
|
42
|
+
>
|
|
43
|
+
<FormSelectOption
|
|
44
|
+
key="select-task"
|
|
45
|
+
value=""
|
|
46
|
+
isPlaceholder
|
|
47
|
+
label={isLoading ? __('Loading...') : __('Select Task')}
|
|
48
|
+
/>
|
|
49
|
+
{taskNames.map(taskName => (
|
|
50
|
+
<FormSelectOption
|
|
51
|
+
key={taskName}
|
|
52
|
+
value={taskName}
|
|
53
|
+
label={taskName}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</FormSelect>
|
|
57
|
+
</FlexItem>
|
|
58
|
+
<FlexItem>
|
|
59
|
+
<Button
|
|
60
|
+
variant="secondary"
|
|
61
|
+
onClick={onReloadTasks}
|
|
62
|
+
isDisabled={isDisabled || isLoading}
|
|
63
|
+
icon={isLoading ? <Spinner size="sm" /> : <SyncIcon />}
|
|
64
|
+
aria-label={__('Reload tasks from OpenBolt')}
|
|
65
|
+
title={__('Reload tasks from OpenBolt. This may take some time.')}
|
|
66
|
+
/>
|
|
67
|
+
</FlexItem>
|
|
68
|
+
</Flex>
|
|
69
|
+
<span id="task-select-helper" className="pf-v5-u-screen-reader">
|
|
70
|
+
{__('Select a OpenBolt task to execute on the specified targets')}
|
|
71
|
+
</span>
|
|
72
|
+
</FormGroup>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
TaskSelect.propTypes = {
|
|
76
|
+
taskNames: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
77
|
+
selectedTask: PropTypes.string.isRequired,
|
|
78
|
+
onTaskChange: PropTypes.func.isRequired,
|
|
79
|
+
onReloadTasks: PropTypes.func.isRequired,
|
|
80
|
+
isLoading: PropTypes.bool.isRequired,
|
|
81
|
+
isDisabled: PropTypes.bool.isRequired,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default TaskSelect;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
3
|
+
import { API } from 'foremanReact/redux/API';
|
|
4
|
+
import { ROUTES } from '../../common/constants';
|
|
5
|
+
import { useShowMessage } from '../../common/helpers';
|
|
6
|
+
|
|
7
|
+
export const useOpenBoltOptions = () => {
|
|
8
|
+
const showMessage = useShowMessage();
|
|
9
|
+
|
|
10
|
+
const [openBoltOptionsMetadata, setOpenBoltOptionsMetadata] = useState({});
|
|
11
|
+
const [openBoltOptions, setOpenBoltOptions] = useState({});
|
|
12
|
+
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
|
|
13
|
+
|
|
14
|
+
const fetchOpenBoltOptions = useCallback(
|
|
15
|
+
async proxyId => {
|
|
16
|
+
if (!proxyId) return null;
|
|
17
|
+
|
|
18
|
+
setIsLoadingOptions(true);
|
|
19
|
+
setOpenBoltOptionsMetadata({});
|
|
20
|
+
setOpenBoltOptions({});
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { data, status } = await API.get(
|
|
24
|
+
`${ROUTES.API.FETCH_OPENBOLT_OPTIONS}?proxy_id=${proxyId}`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (status !== 200) {
|
|
28
|
+
const error = data
|
|
29
|
+
? data.error || JSON.stringify(data)
|
|
30
|
+
: 'Unknown error';
|
|
31
|
+
throw new Error(`HTTP ${status} - ${error}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setOpenBoltOptionsMetadata(data || {});
|
|
35
|
+
|
|
36
|
+
// Set defaults
|
|
37
|
+
const defaults = {};
|
|
38
|
+
Object.entries(data || {}).forEach(([optionName, optionMeta]) => {
|
|
39
|
+
if (optionMeta.default !== undefined) {
|
|
40
|
+
defaults[optionName] = optionMeta.default;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
setOpenBoltOptions(defaults);
|
|
44
|
+
|
|
45
|
+
return data;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
showMessage(__('Failed to load OpenBolt options: ') + error.message);
|
|
48
|
+
return null;
|
|
49
|
+
} finally {
|
|
50
|
+
setIsLoadingOptions(false);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[showMessage]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
openBoltOptionsMetadata,
|
|
58
|
+
openBoltOptions,
|
|
59
|
+
setOpenBoltOptions,
|
|
60
|
+
isLoadingOptions,
|
|
61
|
+
fetchOpenBoltOptions,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
3
|
+
import { API } from 'foremanReact/redux/API';
|
|
4
|
+
import { useShowMessage } from '../../common/helpers';
|
|
5
|
+
|
|
6
|
+
export const useSmartProxies = () => {
|
|
7
|
+
const showMessage = useShowMessage();
|
|
8
|
+
const [smartProxies, setSmartProxies] = useState([]);
|
|
9
|
+
const [isLoadingProxies, setIsLoadingProxies] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const fetchSmartProxies = async () => {
|
|
13
|
+
setIsLoadingProxies(true);
|
|
14
|
+
try {
|
|
15
|
+
const endpoint = `/api/smart_proxies?${new URLSearchParams({
|
|
16
|
+
per_page: 'all',
|
|
17
|
+
search: 'feature=OpenBolt',
|
|
18
|
+
})}`;
|
|
19
|
+
const { data, status } = await API.get(endpoint);
|
|
20
|
+
|
|
21
|
+
if (status !== 200) {
|
|
22
|
+
const error = data
|
|
23
|
+
? data.error || JSON.stringify(data)
|
|
24
|
+
: 'Unknown error';
|
|
25
|
+
throw new Error(`HTTP ${status} - ${error}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setSmartProxies(data.results || []);
|
|
29
|
+
|
|
30
|
+
if (data.results.length === 0) {
|
|
31
|
+
showMessage(
|
|
32
|
+
__(
|
|
33
|
+
'No Smart Proxies found. Please check that one or more proxy has the smart_proxy_openbolt package installed and enabled.'
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
showMessage(__('Failed to load Smart Proxies: ') + error.message);
|
|
39
|
+
} finally {
|
|
40
|
+
setIsLoadingProxies(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
fetchSmartProxies();
|
|
45
|
+
}, [showMessage]);
|
|
46
|
+
|
|
47
|
+
return { smartProxies, isLoadingProxies };
|
|
48
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
3
|
+
import { API } from 'foremanReact/redux/API';
|
|
4
|
+
import { ROUTES } from '../../common/constants';
|
|
5
|
+
import { useShowMessage } from '../../common/helpers';
|
|
6
|
+
|
|
7
|
+
export const useTasksData = () => {
|
|
8
|
+
const showMessage = useShowMessage();
|
|
9
|
+
const [taskMetadata, setTaskMetadata] = useState({});
|
|
10
|
+
const [selectedTask, setSelectedTask] = useState('');
|
|
11
|
+
const [taskParameters, setTaskParameters] = useState({});
|
|
12
|
+
const [isLoadingTasks, setIsLoadingTasks] = useState(false);
|
|
13
|
+
|
|
14
|
+
const fetchTasks = useCallback(
|
|
15
|
+
async (proxyId, forceReload = false) => {
|
|
16
|
+
if (!proxyId) return null;
|
|
17
|
+
|
|
18
|
+
setIsLoadingTasks(true);
|
|
19
|
+
setTaskMetadata({});
|
|
20
|
+
setSelectedTask('');
|
|
21
|
+
setTaskParameters({});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const endpoint = forceReload
|
|
25
|
+
? ROUTES.API.RELOAD_TASKS
|
|
26
|
+
: ROUTES.API.FETCH_TASKS;
|
|
27
|
+
const { data, status } = await API.get(
|
|
28
|
+
`${endpoint}?proxy_id=${proxyId}`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (status !== 200) {
|
|
32
|
+
const error = data
|
|
33
|
+
? data.error || JSON.stringify(data)
|
|
34
|
+
: 'Unknown error';
|
|
35
|
+
throw new Error(`HTTP ${status} - ${error}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setTaskMetadata(data || {});
|
|
39
|
+
|
|
40
|
+
if (forceReload) {
|
|
41
|
+
showMessage(__('Tasks reloaded successfully'), 'success');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return data;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
showMessage(__('Failed to load tasks: ') + error.message);
|
|
47
|
+
return null;
|
|
48
|
+
} finally {
|
|
49
|
+
setIsLoadingTasks(false);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[showMessage]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
taskMetadata,
|
|
57
|
+
selectedTask,
|
|
58
|
+
setSelectedTask,
|
|
59
|
+
taskParameters,
|
|
60
|
+
setTaskParameters,
|
|
61
|
+
isLoadingTasks,
|
|
62
|
+
fetchTasks,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// TODO: More a11y tags
|
|
2
|
+
import React, { useState, useCallback } from 'react';
|
|
3
|
+
import { useHistory } from 'react-router-dom';
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
+
|
|
6
|
+
import { API } from 'foremanReact/redux/API';
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
CardBody,
|
|
11
|
+
Divider,
|
|
12
|
+
Flex,
|
|
13
|
+
FlexItem,
|
|
14
|
+
Form,
|
|
15
|
+
Grid,
|
|
16
|
+
GridItem,
|
|
17
|
+
Label,
|
|
18
|
+
Stack,
|
|
19
|
+
StackItem,
|
|
20
|
+
} from '@patternfly/react-core';
|
|
21
|
+
|
|
22
|
+
import { ROUTES } from '../common/constants';
|
|
23
|
+
import SmartProxySelect from './SmartProxySelect';
|
|
24
|
+
import TaskSelect from './TaskSelect';
|
|
25
|
+
import ParametersSection from './ParametersSection';
|
|
26
|
+
import OpenBoltOptionsSection from './OpenBoltOptionsSection';
|
|
27
|
+
import HostSelector from './HostSelector';
|
|
28
|
+
import { useSmartProxies } from './hooks/useSmartProxies';
|
|
29
|
+
import { useTasksData } from './hooks/useTasksData';
|
|
30
|
+
import { useOpenBoltOptions } from './hooks/useOpenBoltOptions';
|
|
31
|
+
import { useShowMessage } from '../common/helpers';
|
|
32
|
+
|
|
33
|
+
const LaunchTask = () => {
|
|
34
|
+
const history = useHistory();
|
|
35
|
+
const showMessage = useShowMessage();
|
|
36
|
+
|
|
37
|
+
/* States */
|
|
38
|
+
const [selectedProxy, setSelectedProxy] = useState('');
|
|
39
|
+
const [targets, setTargets] = useState([]);
|
|
40
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
41
|
+
|
|
42
|
+
/* Custom hooks for data fetching */
|
|
43
|
+
// The strategy here is to manage all data fetching and state within
|
|
44
|
+
// these custom hooks. The rest of this component handles orchestration
|
|
45
|
+
// of that data.
|
|
46
|
+
const { smartProxies, isLoadingProxies } = useSmartProxies();
|
|
47
|
+
const {
|
|
48
|
+
taskMetadata,
|
|
49
|
+
selectedTask,
|
|
50
|
+
setSelectedTask,
|
|
51
|
+
taskParameters,
|
|
52
|
+
setTaskParameters,
|
|
53
|
+
isLoadingTasks,
|
|
54
|
+
fetchTasks,
|
|
55
|
+
} = useTasksData();
|
|
56
|
+
const {
|
|
57
|
+
openBoltOptionsMetadata,
|
|
58
|
+
openBoltOptions,
|
|
59
|
+
setOpenBoltOptions,
|
|
60
|
+
isLoadingOptions,
|
|
61
|
+
fetchOpenBoltOptions,
|
|
62
|
+
} = useOpenBoltOptions();
|
|
63
|
+
|
|
64
|
+
/* Event handlers */
|
|
65
|
+
const handleProxyChange = useCallback(
|
|
66
|
+
(_event, value) => {
|
|
67
|
+
setSelectedProxy(value);
|
|
68
|
+
if (value) {
|
|
69
|
+
fetchTasks(value);
|
|
70
|
+
fetchOpenBoltOptions(value);
|
|
71
|
+
} else {
|
|
72
|
+
setSelectedTask('');
|
|
73
|
+
setTaskParameters({});
|
|
74
|
+
setOpenBoltOptions({});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[
|
|
78
|
+
fetchTasks,
|
|
79
|
+
fetchOpenBoltOptions,
|
|
80
|
+
setSelectedTask,
|
|
81
|
+
setTaskParameters,
|
|
82
|
+
setOpenBoltOptions,
|
|
83
|
+
]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const handleTaskChange = useCallback(
|
|
87
|
+
(_event, value) => {
|
|
88
|
+
setSelectedTask(value);
|
|
89
|
+
// TODO: Do we want to set boolean to default false here when
|
|
90
|
+
// a default is not provided?
|
|
91
|
+
if (value && taskMetadata[value]) {
|
|
92
|
+
const defaults = {};
|
|
93
|
+
const params = taskMetadata[value].parameters || {};
|
|
94
|
+
Object.entries(params).forEach(([paramName, paramMeta]) => {
|
|
95
|
+
if (paramMeta.default !== undefined) {
|
|
96
|
+
defaults[paramName] = paramMeta.default;
|
|
97
|
+
} else if (
|
|
98
|
+
['boolean', 'optional[boolean]'].includes(
|
|
99
|
+
paramMeta.type.toLowerCase()
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
defaults[paramName] = false;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
setTaskParameters(defaults);
|
|
106
|
+
} else {
|
|
107
|
+
setTaskParameters({});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[setSelectedTask, setTaskParameters, taskMetadata]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const handleParameterChange = useCallback(
|
|
114
|
+
(paramName, value) => {
|
|
115
|
+
setTaskParameters(prev => ({ ...prev, [paramName]: value }));
|
|
116
|
+
},
|
|
117
|
+
[setTaskParameters]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleOptionChange = useCallback(
|
|
121
|
+
(optionName, value) => {
|
|
122
|
+
setOpenBoltOptions(prev => ({ ...prev, [optionName]: value }));
|
|
123
|
+
},
|
|
124
|
+
[setOpenBoltOptions]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const handleReloadTasks = useCallback(() => {
|
|
128
|
+
if (selectedProxy) {
|
|
129
|
+
fetchTasks(selectedProxy, true);
|
|
130
|
+
}
|
|
131
|
+
}, [selectedProxy, fetchTasks]);
|
|
132
|
+
|
|
133
|
+
const handleTargetsChange = useCallback(
|
|
134
|
+
targetArray => {
|
|
135
|
+
setTargets(targetArray);
|
|
136
|
+
},
|
|
137
|
+
[setTargets]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const handleSubmit = useCallback(
|
|
141
|
+
async e => {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
|
|
144
|
+
if (!selectedProxy || !selectedTask || targets.length === 0) {
|
|
145
|
+
showMessage(__('Please select a proxy, task, and enter targets.'));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setIsSubmitting(true);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const { transport } = openBoltOptions;
|
|
153
|
+
const visibleOptions = {};
|
|
154
|
+
Object.entries(openBoltOptionsMetadata).forEach(
|
|
155
|
+
([optionName, metadata]) => {
|
|
156
|
+
const transports = Array.isArray(metadata.transport)
|
|
157
|
+
? metadata.transport
|
|
158
|
+
: null;
|
|
159
|
+
const isVisible =
|
|
160
|
+
optionName === 'transport' ||
|
|
161
|
+
!transports ||
|
|
162
|
+
transports.includes(transport);
|
|
163
|
+
if (isVisible && openBoltOptions[optionName] !== undefined) {
|
|
164
|
+
visibleOptions[optionName] = openBoltOptions[optionName];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const body = {
|
|
170
|
+
proxy_id: selectedProxy,
|
|
171
|
+
task_name: selectedTask,
|
|
172
|
+
targets: targets.join(','),
|
|
173
|
+
params: taskParameters,
|
|
174
|
+
options: visibleOptions,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const { data, status } = await API.post(ROUTES.API.LAUNCH_TASK, body);
|
|
178
|
+
|
|
179
|
+
// TODO: On non-200, the post above automatically throws an exception, so
|
|
180
|
+
// figure out how to handle it instead to extract the message in the
|
|
181
|
+
// response body.
|
|
182
|
+
if (status !== 200) {
|
|
183
|
+
const error = data
|
|
184
|
+
? data.error || JSON.stringify(data)
|
|
185
|
+
: 'Unknown error';
|
|
186
|
+
throw new Error(`HTTP ${status} - ${error}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const selectedProxyData = smartProxies.find(
|
|
190
|
+
p => p.id.toString() === selectedProxy.toString()
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
history.push({
|
|
194
|
+
pathname: ROUTES.PAGES.TASK_EXECUTION,
|
|
195
|
+
search: new URLSearchParams({
|
|
196
|
+
proxy_id: selectedProxy,
|
|
197
|
+
job_id: data.job_id,
|
|
198
|
+
proxy_name: selectedProxyData?.name || 'Unknown',
|
|
199
|
+
}).toString(),
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const errorMessage =
|
|
203
|
+
error.response?.data?.error ||
|
|
204
|
+
error.message ||
|
|
205
|
+
__('Unknown error occurred');
|
|
206
|
+
showMessage(__('Failed to launch task: ') + errorMessage);
|
|
207
|
+
} finally {
|
|
208
|
+
setIsSubmitting(false);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
[
|
|
212
|
+
selectedProxy,
|
|
213
|
+
selectedTask,
|
|
214
|
+
targets,
|
|
215
|
+
taskParameters,
|
|
216
|
+
openBoltOptions,
|
|
217
|
+
openBoltOptionsMetadata,
|
|
218
|
+
smartProxies,
|
|
219
|
+
history,
|
|
220
|
+
showMessage,
|
|
221
|
+
]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
/* Rendering */
|
|
225
|
+
const isFormValid =
|
|
226
|
+
selectedProxy &&
|
|
227
|
+
selectedTask &&
|
|
228
|
+
targets.length > 0 &&
|
|
229
|
+
!isLoadingTasks &&
|
|
230
|
+
!isLoadingOptions &&
|
|
231
|
+
!isSubmitting;
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className="openbolt-task-form">
|
|
235
|
+
<Form onSubmit={handleSubmit}>
|
|
236
|
+
<Grid hasGutter>
|
|
237
|
+
<GridItem span={12}>
|
|
238
|
+
<Flex
|
|
239
|
+
hasGutter
|
|
240
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
241
|
+
justifyContent={{ default: 'justifyContentSpaceBetween' }}
|
|
242
|
+
>
|
|
243
|
+
<FlexItem>
|
|
244
|
+
<Flex
|
|
245
|
+
spaceItems={{ default: 'spaceItemsSm' }}
|
|
246
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
247
|
+
wrap={{ default: 'wrap' }}
|
|
248
|
+
>
|
|
249
|
+
{targets?.length > 0 && (
|
|
250
|
+
<Label color="gold">
|
|
251
|
+
{__('Targets')}: {targets.length}
|
|
252
|
+
</Label>
|
|
253
|
+
)}
|
|
254
|
+
{selectedTask && (
|
|
255
|
+
<Label color="gold">
|
|
256
|
+
{__('Task')}: {selectedTask}
|
|
257
|
+
</Label>
|
|
258
|
+
)}
|
|
259
|
+
</Flex>
|
|
260
|
+
</FlexItem>
|
|
261
|
+
<FlexItem>
|
|
262
|
+
<Button
|
|
263
|
+
type="submit"
|
|
264
|
+
variant="primary"
|
|
265
|
+
isDisabled={!isFormValid}
|
|
266
|
+
isLoading={isSubmitting}
|
|
267
|
+
>
|
|
268
|
+
{`🚀 ${__('Launch Task')}`}
|
|
269
|
+
</Button>
|
|
270
|
+
</FlexItem>
|
|
271
|
+
</Flex>
|
|
272
|
+
<Divider />
|
|
273
|
+
</GridItem>
|
|
274
|
+
<GridItem span={12} md={7} lg={8}>
|
|
275
|
+
<Card isFlat>
|
|
276
|
+
<CardBody>
|
|
277
|
+
<Stack hasGutter>
|
|
278
|
+
<StackItem>
|
|
279
|
+
<SmartProxySelect
|
|
280
|
+
smartProxies={smartProxies}
|
|
281
|
+
selectedProxy={selectedProxy}
|
|
282
|
+
onProxyChange={handleProxyChange}
|
|
283
|
+
isLoading={isLoadingProxies}
|
|
284
|
+
/>
|
|
285
|
+
</StackItem>
|
|
286
|
+
<StackItem>
|
|
287
|
+
<HostSelector
|
|
288
|
+
onChange={handleTargetsChange}
|
|
289
|
+
targetCount={targets.length}
|
|
290
|
+
/>
|
|
291
|
+
</StackItem>
|
|
292
|
+
<StackItem>
|
|
293
|
+
<TaskSelect
|
|
294
|
+
taskNames={Object.keys(taskMetadata || {})}
|
|
295
|
+
selectedTask={selectedTask}
|
|
296
|
+
onTaskChange={handleTaskChange}
|
|
297
|
+
onReloadTasks={handleReloadTasks}
|
|
298
|
+
isLoading={isLoadingTasks}
|
|
299
|
+
isDisabled={!selectedProxy}
|
|
300
|
+
/>
|
|
301
|
+
</StackItem>
|
|
302
|
+
<StackItem>
|
|
303
|
+
<ParametersSection
|
|
304
|
+
selectedTask={selectedTask}
|
|
305
|
+
taskMetadata={taskMetadata}
|
|
306
|
+
taskParameters={taskParameters}
|
|
307
|
+
onParameterChange={handleParameterChange}
|
|
308
|
+
/>
|
|
309
|
+
</StackItem>
|
|
310
|
+
</Stack>
|
|
311
|
+
</CardBody>
|
|
312
|
+
</Card>
|
|
313
|
+
</GridItem>
|
|
314
|
+
<GridItem span={12} md={5} lg={4}>
|
|
315
|
+
<Card isFlat>
|
|
316
|
+
<CardBody>
|
|
317
|
+
<OpenBoltOptionsSection
|
|
318
|
+
selectedProxy={selectedProxy}
|
|
319
|
+
openBoltOptionsMetadata={openBoltOptionsMetadata}
|
|
320
|
+
openBoltOptions={openBoltOptions}
|
|
321
|
+
onOptionChange={handleOptionChange}
|
|
322
|
+
isLoading={isLoadingOptions}
|
|
323
|
+
/>
|
|
324
|
+
</CardBody>
|
|
325
|
+
</Card>
|
|
326
|
+
</GridItem>
|
|
327
|
+
</Grid>
|
|
328
|
+
</Form>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export default LaunchTask;
|