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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +619 -0
  3. data/README.md +46 -0
  4. data/Rakefile +106 -0
  5. data/app/controllers/foreman_openbolt/task_controller.rb +298 -0
  6. data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +40 -0
  7. data/app/lib/actions/foreman_openbolt/poll_task_status.rb +151 -0
  8. data/app/models/foreman_openbolt/task_job.rb +110 -0
  9. data/app/views/foreman_openbolt/react_page.html.erb +1 -0
  10. data/config/routes.rb +24 -0
  11. data/db/migrate/20250819000000_create_openbolt_task_jobs.rb +25 -0
  12. data/db/migrate/20250925000000_add_command_to_openbolt_task_jobs.rb +7 -0
  13. data/db/migrate/20251001000000_add_task_description_to_task_jobs.rb +7 -0
  14. data/db/seeds.d/001_add_openbolt_feature.rb +4 -0
  15. data/lib/foreman_openbolt/engine.rb +169 -0
  16. data/lib/foreman_openbolt/version.rb +5 -0
  17. data/lib/foreman_openbolt.rb +7 -0
  18. data/lib/proxy_api/openbolt.rb +53 -0
  19. data/lib/tasks/foreman_openbolt_tasks.rake +48 -0
  20. data/locale/Makefile +73 -0
  21. data/locale/en/foreman_openbolt.po +19 -0
  22. data/locale/foreman_openbolt.pot +19 -0
  23. data/locale/gemspec.rb +7 -0
  24. data/package.json +41 -0
  25. data/test/factories/foreman_openbolt_factories.rb +7 -0
  26. data/test/test_plugin_helper.rb +8 -0
  27. data/test/unit/foreman_openbolt_test.rb +13 -0
  28. data/webpack/global_index.js +4 -0
  29. data/webpack/global_test_setup.js +11 -0
  30. data/webpack/index.js +19 -0
  31. data/webpack/src/Components/LaunchTask/EmptyContent.js +24 -0
  32. data/webpack/src/Components/LaunchTask/FieldTable.js +147 -0
  33. data/webpack/src/Components/LaunchTask/HostSelector/HostSearch.js +29 -0
  34. data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +208 -0
  35. data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +113 -0
  36. data/webpack/src/Components/LaunchTask/HostSelector/hostgroups.gql +9 -0
  37. data/webpack/src/Components/LaunchTask/HostSelector/hosts.gql +10 -0
  38. data/webpack/src/Components/LaunchTask/HostSelector/index.js +261 -0
  39. data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +116 -0
  40. data/webpack/src/Components/LaunchTask/ParameterField.js +145 -0
  41. data/webpack/src/Components/LaunchTask/ParametersSection.js +66 -0
  42. data/webpack/src/Components/LaunchTask/SmartProxySelect.js +51 -0
  43. data/webpack/src/Components/LaunchTask/TaskSelect.js +84 -0
  44. data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +63 -0
  45. data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +48 -0
  46. data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +64 -0
  47. data/webpack/src/Components/LaunchTask/index.js +333 -0
  48. data/webpack/src/Components/TaskExecution/ExecutionDetails.js +188 -0
  49. data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +99 -0
  50. data/webpack/src/Components/TaskExecution/LoadingIndicator.js +51 -0
  51. data/webpack/src/Components/TaskExecution/ResultDisplay.js +174 -0
  52. data/webpack/src/Components/TaskExecution/TaskDetails.js +99 -0
  53. data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +142 -0
  54. data/webpack/src/Components/TaskExecution/index.js +130 -0
  55. data/webpack/src/Components/TaskHistory/TaskPopover.js +95 -0
  56. data/webpack/src/Components/TaskHistory/index.js +199 -0
  57. data/webpack/src/Components/common/HostsPopover.js +49 -0
  58. data/webpack/src/Components/common/constants.js +44 -0
  59. data/webpack/src/Components/common/helpers.js +19 -0
  60. data/webpack/src/Pages/LaunchTaskPage.js +12 -0
  61. data/webpack/src/Pages/TaskExecutionPage.js +12 -0
  62. data/webpack/src/Pages/TaskHistoryPage.js +12 -0
  63. data/webpack/src/Router/routes.js +30 -0
  64. data/webpack/test_setup.js +17 -0
  65. data/webpack/webpack.config.js +7 -0
  66. 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;