foreman_openbolt 1.0.0 → 1.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +190 -19
  3. data/Rakefile +17 -93
  4. data/app/controllers/foreman_openbolt/task_controller.rb +61 -49
  5. data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +11 -10
  6. data/app/lib/actions/foreman_openbolt/poll_task_status.rb +70 -60
  7. data/app/models/foreman_openbolt/task_job.rb +16 -17
  8. data/config/routes.rb +0 -1
  9. data/lib/foreman_openbolt/engine.rb +11 -11
  10. data/lib/foreman_openbolt/version.rb +1 -1
  11. data/lib/proxy_api/openbolt.rb +25 -9
  12. data/lib/tasks/foreman_openbolt_tasks.rake +1 -22
  13. data/locale/gemspec.rb +1 -1
  14. data/package.json +11 -15
  15. data/test/acceptance/acceptance_helper.rb +146 -0
  16. data/test/acceptance/docker/docker-compose.yml +69 -0
  17. data/test/acceptance/docker/foreman/Dockerfile +45 -0
  18. data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
  19. data/test/acceptance/docker/target/Dockerfile +29 -0
  20. data/test/acceptance/docker/target/entrypoint.sh +11 -0
  21. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
  22. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
  23. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
  24. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
  25. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
  26. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
  27. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
  28. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
  29. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
  30. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
  31. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
  32. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
  33. data/test/acceptance/fixtures/openbolt.yml +7 -0
  34. data/test/acceptance/tests/error_handling_test.rb +40 -0
  35. data/test/acceptance/tests/host_selector_test.rb +31 -0
  36. data/test/acceptance/tests/launch_task_test.rb +96 -0
  37. data/test/acceptance/tests/parameter_table_test.rb +61 -0
  38. data/test/acceptance/tests/settings_test.rb +95 -0
  39. data/test/acceptance/tests/ssh_options_test.rb +77 -0
  40. data/test/acceptance/tests/task_execution_test.rb +40 -0
  41. data/test/acceptance/tests/task_history_test.rb +84 -0
  42. data/test/acceptance/tests/transport_options_test.rb +121 -0
  43. data/test/test_plugin_helper.rb +12 -3
  44. data/test/unit/controllers/task_controller_test.rb +351 -0
  45. data/test/unit/docker/Dockerfile +47 -0
  46. data/test/unit/docker/docker-compose.yml +33 -0
  47. data/test/unit/docker/entrypoint.sh +4 -0
  48. data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
  49. data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
  50. data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
  51. data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
  52. data/test/unit/models/task_job_test.rb +278 -0
  53. data/webpack/__mocks__/foremanReact/common/I18n.js +15 -0
  54. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +6 -0
  55. data/webpack/__mocks__/foremanReact/redux/API/index.js +11 -0
  56. data/webpack/src/Components/LaunchTask/FieldTable.js +8 -5
  57. data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +74 -62
  58. data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +11 -13
  59. data/webpack/src/Components/LaunchTask/HostSelector/index.js +28 -33
  60. data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +3 -2
  61. data/webpack/src/Components/LaunchTask/ParameterField.js +2 -0
  62. data/webpack/src/Components/LaunchTask/SmartProxySelect.js +2 -1
  63. data/webpack/src/Components/LaunchTask/TaskSelect.js +3 -3
  64. data/webpack/src/Components/LaunchTask/__tests__/EmptyContent.test.js +10 -0
  65. data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +83 -0
  66. data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +86 -0
  67. data/webpack/src/Components/LaunchTask/__tests__/ParametersSection.test.js +50 -0
  68. data/webpack/src/Components/LaunchTask/__tests__/SmartProxySelect.test.js +63 -0
  69. data/webpack/src/Components/LaunchTask/__tests__/TaskSelect.test.js +39 -0
  70. data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +90 -0
  71. data/webpack/src/Components/LaunchTask/hooks/__tests__/useSmartProxies.test.js +69 -0
  72. data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +103 -0
  73. data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +9 -11
  74. data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +12 -13
  75. data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +6 -13
  76. data/webpack/src/Components/LaunchTask/index.js +9 -27
  77. data/webpack/src/Components/TaskExecution/ExecutionDetails.js +29 -29
  78. data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +9 -10
  79. data/webpack/src/Components/TaskExecution/LoadingIndicator.js +7 -2
  80. data/webpack/src/Components/TaskExecution/ResultDisplay.js +13 -17
  81. data/webpack/src/Components/TaskExecution/TaskDetails.js +58 -67
  82. data/webpack/src/Components/TaskExecution/__tests__/ExecutionDetails.test.js +47 -0
  83. data/webpack/src/Components/TaskExecution/__tests__/ExecutionDisplay.test.js +29 -0
  84. data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +25 -0
  85. data/webpack/src/Components/TaskExecution/__tests__/ResultDisplay.test.js +28 -0
  86. data/webpack/src/Components/TaskExecution/__tests__/TaskDetails.test.js +38 -0
  87. data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +80 -0
  88. data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +177 -0
  89. data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +34 -33
  90. data/webpack/src/Components/TaskExecution/index.js +10 -12
  91. data/webpack/src/Components/TaskHistory/TaskPopover.js +9 -12
  92. data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +109 -0
  93. data/webpack/src/Components/TaskHistory/__tests__/TaskPopover.test.js +26 -0
  94. data/webpack/src/Components/TaskHistory/index.js +21 -29
  95. data/webpack/src/Components/common/HostsPopover.js +12 -3
  96. data/webpack/src/Components/common/__tests__/HostsPopover.test.js +20 -0
  97. data/webpack/src/Components/common/__tests__/helpers.test.js +135 -0
  98. data/webpack/src/Components/common/helpers.js +34 -5
  99. data/webpack/test_setup.js +34 -11
  100. metadata +65 -87
  101. data/test/factories/foreman_openbolt_factories.rb +0 -7
  102. data/test/unit/foreman_openbolt_test.rb +0 -13
  103. data/webpack/global_test_setup.js +0 -11
  104. data/webpack/webpack.config.js +0 -7
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import ParametersSection from '../ParametersSection';
4
+
5
+ describe('ParametersSection', () => {
6
+ test('shows empty state when no task is selected', () => {
7
+ render(
8
+ <ParametersSection
9
+ selectedTask=""
10
+ taskMetadata={{}}
11
+ taskParameters={{}}
12
+ onParameterChange={jest.fn()}
13
+ />
14
+ );
15
+ expect(
16
+ screen.getByText('Select a task to see parameters')
17
+ ).toBeInTheDocument();
18
+ });
19
+
20
+ test('shows empty state when task has no parameters', () => {
21
+ render(
22
+ <ParametersSection
23
+ selectedTask="test::task"
24
+ taskMetadata={{ 'test::task': { parameters: {} } }}
25
+ taskParameters={{}}
26
+ onParameterChange={jest.fn()}
27
+ />
28
+ );
29
+ expect(screen.getByText('This task has no parameters')).toBeInTheDocument();
30
+ });
31
+
32
+ test('renders parameter fields when task has parameters', () => {
33
+ const metadata = {
34
+ 'test::task': {
35
+ parameters: {
36
+ name: { type: 'String', description: 'Package name' },
37
+ },
38
+ },
39
+ };
40
+ render(
41
+ <ParametersSection
42
+ selectedTask="test::task"
43
+ taskMetadata={metadata}
44
+ taskParameters={{ name: 'nginx' }}
45
+ onParameterChange={jest.fn()}
46
+ />
47
+ );
48
+ expect(screen.getByText('name')).toBeInTheDocument();
49
+ });
50
+ });
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import SmartProxySelect from '../SmartProxySelect';
4
+
5
+ const proxies = [
6
+ { id: 1, name: 'proxy-one' },
7
+ { id: 2, name: 'proxy-two' },
8
+ ];
9
+
10
+ describe('SmartProxySelect', () => {
11
+ test('renders proxy options', () => {
12
+ render(
13
+ <SmartProxySelect
14
+ smartProxies={proxies}
15
+ selectedProxy=""
16
+ onProxyChange={jest.fn()}
17
+ isLoading={false}
18
+ />
19
+ );
20
+ expect(screen.getByText('proxy-one')).toBeInTheDocument();
21
+ expect(screen.getByText('proxy-two')).toBeInTheDocument();
22
+ });
23
+
24
+ test('shows loading placeholder when loading', () => {
25
+ render(
26
+ <SmartProxySelect
27
+ smartProxies={[]}
28
+ selectedProxy=""
29
+ onProxyChange={jest.fn()}
30
+ isLoading
31
+ />
32
+ );
33
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
34
+ });
35
+
36
+ test('disables select when loading', () => {
37
+ const { container } = render(
38
+ <SmartProxySelect
39
+ smartProxies={proxies}
40
+ selectedProxy=""
41
+ onProxyChange={jest.fn()}
42
+ isLoading
43
+ />
44
+ );
45
+ const select = container.querySelector('select');
46
+ expect(select).toBeDisabled();
47
+ });
48
+
49
+ test('calls onProxyChange when selection changes', () => {
50
+ const handleChange = jest.fn();
51
+ const { container } = render(
52
+ <SmartProxySelect
53
+ smartProxies={proxies}
54
+ selectedProxy=""
55
+ onProxyChange={handleChange}
56
+ isLoading={false}
57
+ />
58
+ );
59
+ const select = container.querySelector('select');
60
+ fireEvent.change(select, { target: { value: '1' } });
61
+ expect(handleChange).toHaveBeenCalledWith(expect.anything(), '1');
62
+ });
63
+ });
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import TaskSelect from '../TaskSelect';
4
+
5
+ const defaultProps = {
6
+ taskNames: ['mymod::install', 'mymod::mytask'],
7
+ selectedTask: '',
8
+ onTaskChange: jest.fn(),
9
+ onReloadTasks: jest.fn(),
10
+ isLoading: false,
11
+ isDisabled: false,
12
+ };
13
+
14
+ describe('TaskSelect', () => {
15
+ test('renders task options', () => {
16
+ render(<TaskSelect {...defaultProps} />);
17
+ expect(screen.getByText('mymod::install')).toBeInTheDocument();
18
+ expect(screen.getByText('mymod::mytask')).toBeInTheDocument();
19
+ });
20
+
21
+ test('shows loading placeholder when loading', () => {
22
+ render(<TaskSelect {...defaultProps} isLoading />);
23
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
24
+ });
25
+
26
+ test('disables select and button when disabled', () => {
27
+ const { container } = render(<TaskSelect {...defaultProps} isDisabled />);
28
+ const select = container.querySelector('select');
29
+ expect(select).toBeDisabled();
30
+ });
31
+
32
+ test('calls onReloadTasks when reload button is clicked', () => {
33
+ const handleReload = jest.fn();
34
+ render(<TaskSelect {...defaultProps} onReloadTasks={handleReload} />);
35
+ const reloadButton = screen.getByLabelText('Reload tasks from OpenBolt');
36
+ fireEvent.click(reloadButton);
37
+ expect(handleReload).toHaveBeenCalled();
38
+ });
39
+ });
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { renderHook, act } from '@testing-library/react-hooks';
3
+ import { Provider } from 'react-redux';
4
+ import { createStore } from 'redux';
5
+ import { API } from 'foremanReact/redux/API';
6
+ import { addToast } from 'foremanReact/components/ToastsList';
7
+ import { useOpenBoltOptions } from '../useOpenBoltOptions';
8
+
9
+ const mockStore = createStore(() => ({}));
10
+ const wrapper = ({ children }) => (
11
+ <Provider store={mockStore}>{children}</Provider>
12
+ );
13
+
14
+ afterEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ describe('useOpenBoltOptions', () => {
19
+ test('returns null and does not call API when proxyId is falsy', async () => {
20
+ const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
21
+
22
+ let returnValue;
23
+ await act(async () => {
24
+ returnValue = await result.current.fetchOpenBoltOptions(null);
25
+ });
26
+
27
+ expect(returnValue).toBeNull();
28
+ expect(API.get).not.toHaveBeenCalled();
29
+ });
30
+
31
+ test('fetches options and extracts defaults', async () => {
32
+ const options = {
33
+ transport: { type: 'string', default: 'ssh' },
34
+ user: { type: 'string' },
35
+ verbose: { type: 'boolean', default: false },
36
+ };
37
+ API.get.mockResolvedValue({ data: options });
38
+
39
+ const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
40
+
41
+ await act(async () => {
42
+ await result.current.fetchOpenBoltOptions(42);
43
+ });
44
+
45
+ expect(result.current.openBoltOptionsMetadata).toEqual(options);
46
+ expect(result.current.openBoltOptions).toEqual({
47
+ transport: 'ssh',
48
+ verbose: false,
49
+ });
50
+ expect(result.current.openBoltOptions.user).toBeUndefined();
51
+ });
52
+
53
+ test('resets state before fetching', async () => {
54
+ API.get.mockResolvedValue({ data: { transport: { default: 'ssh' } } });
55
+
56
+ const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
57
+
58
+ await act(async () => {
59
+ await result.current.fetchOpenBoltOptions(1);
60
+ });
61
+ expect(result.current.openBoltOptions).toEqual({ transport: 'ssh' });
62
+
63
+ API.get.mockResolvedValue({ data: { user: { default: 'root' } } });
64
+ await act(async () => {
65
+ await result.current.fetchOpenBoltOptions(2);
66
+ });
67
+ expect(result.current.openBoltOptions).toEqual({ user: 'root' });
68
+ expect(result.current.openBoltOptions.transport).toBeUndefined();
69
+ });
70
+
71
+ test('handles errors and shows toast', async () => {
72
+ API.get.mockRejectedValue({ message: 'Connection refused' });
73
+
74
+ const { result } = renderHook(() => useOpenBoltOptions(), { wrapper });
75
+
76
+ let returnValue;
77
+ await act(async () => {
78
+ returnValue = await result.current.fetchOpenBoltOptions(42);
79
+ });
80
+
81
+ expect(returnValue).toBeNull();
82
+ expect(result.current.isLoadingOptions).toBe(false);
83
+ expect(addToast).toHaveBeenCalledWith(
84
+ expect.objectContaining({
85
+ type: 'danger',
86
+ message: expect.stringContaining('Failed to load OpenBolt options'),
87
+ })
88
+ );
89
+ });
90
+ });
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-hooks';
3
+ import { Provider } from 'react-redux';
4
+ import { createStore } from 'redux';
5
+ import { API } from 'foremanReact/redux/API';
6
+ import { addToast } from 'foremanReact/components/ToastsList';
7
+ import { useSmartProxies } from '../useSmartProxies';
8
+
9
+ const mockStore = createStore(() => ({}));
10
+ const wrapper = ({ children }) => (
11
+ <Provider store={mockStore}>{children}</Provider>
12
+ );
13
+
14
+ afterEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ describe('useSmartProxies', () => {
19
+ test('fetches proxies on mount and returns results', async () => {
20
+ const proxies = [
21
+ { id: 1, name: 'proxy1' },
22
+ { id: 2, name: 'proxy2' },
23
+ ];
24
+ API.get.mockResolvedValue({ data: { results: proxies } });
25
+
26
+ const { result, waitForNextUpdate } = renderHook(() => useSmartProxies(), {
27
+ wrapper,
28
+ });
29
+
30
+ expect(result.current.isLoadingProxies).toBe(true);
31
+
32
+ await waitForNextUpdate();
33
+
34
+ expect(result.current.isLoadingProxies).toBe(false);
35
+ expect(result.current.smartProxies).toEqual(proxies);
36
+ expect(API.get).toHaveBeenCalledWith(
37
+ expect.stringContaining('/api/smart_proxies')
38
+ );
39
+ });
40
+
41
+ test('returns empty array when no proxies found', async () => {
42
+ API.get.mockResolvedValue({ data: { results: [] } });
43
+
44
+ const { result, waitForNextUpdate } = renderHook(() => useSmartProxies(), {
45
+ wrapper,
46
+ });
47
+ await waitForNextUpdate();
48
+
49
+ expect(result.current.smartProxies).toEqual([]);
50
+ });
51
+
52
+ test('handles fetch error and shows toast', async () => {
53
+ API.get.mockRejectedValue({ message: 'Network error' });
54
+
55
+ const { result, waitForNextUpdate } = renderHook(() => useSmartProxies(), {
56
+ wrapper,
57
+ });
58
+ await waitForNextUpdate();
59
+
60
+ expect(result.current.isLoadingProxies).toBe(false);
61
+ expect(result.current.smartProxies).toEqual([]);
62
+ expect(addToast).toHaveBeenCalledWith(
63
+ expect.objectContaining({
64
+ type: 'danger',
65
+ message: expect.stringContaining('Failed to load Smart Proxies'),
66
+ })
67
+ );
68
+ });
69
+ });
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { renderHook, act } from '@testing-library/react-hooks';
3
+ import { Provider } from 'react-redux';
4
+ import { createStore } from 'redux';
5
+ import { API } from 'foremanReact/redux/API';
6
+ import { addToast } from 'foremanReact/components/ToastsList';
7
+ import { useTasksData } from '../useTasksData';
8
+
9
+ const mockStore = createStore(() => ({}));
10
+ const wrapper = ({ children }) => (
11
+ <Provider store={mockStore}>{children}</Provider>
12
+ );
13
+
14
+ afterEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ describe('useTasksData', () => {
19
+ test('fetchTasks returns null and does not call API when proxyId is falsy', async () => {
20
+ const { result } = renderHook(() => useTasksData(), { wrapper });
21
+
22
+ let returnValue;
23
+ await act(async () => {
24
+ returnValue = await result.current.fetchTasks(null);
25
+ });
26
+
27
+ expect(returnValue).toBeNull();
28
+ expect(API.get).not.toHaveBeenCalled();
29
+ });
30
+
31
+ test('fetchTasks fetches tasks from the normal endpoint', async () => {
32
+ const tasks = { 'mymod::install': { description: 'Install a package' } };
33
+ API.get.mockResolvedValue({ data: tasks });
34
+
35
+ const { result } = renderHook(() => useTasksData(), { wrapper });
36
+
37
+ await act(async () => {
38
+ await result.current.fetchTasks(42);
39
+ });
40
+
41
+ expect(API.get).toHaveBeenCalledWith(
42
+ expect.stringContaining('fetch_tasks')
43
+ );
44
+ expect(API.get).toHaveBeenCalledWith(
45
+ expect.stringContaining('proxy_id=42')
46
+ );
47
+ expect(result.current.taskMetadata).toEqual(tasks);
48
+ expect(result.current.isLoadingTasks).toBe(false);
49
+ });
50
+
51
+ test('fetchTasks uses reload endpoint when forceReload is true', async () => {
52
+ API.get.mockResolvedValue({ data: {} });
53
+
54
+ const { result } = renderHook(() => useTasksData(), { wrapper });
55
+
56
+ await act(async () => {
57
+ await result.current.fetchTasks(42, true);
58
+ });
59
+
60
+ expect(API.get).toHaveBeenCalledWith(
61
+ expect.stringContaining('reload_tasks')
62
+ );
63
+ });
64
+
65
+ test('fetchTasks resets state before fetching', async () => {
66
+ API.get.mockResolvedValue({ data: { 'task::one': {} } });
67
+
68
+ const { result } = renderHook(() => useTasksData(), { wrapper });
69
+
70
+ // First fetch
71
+ await act(async () => {
72
+ await result.current.fetchTasks(1);
73
+ });
74
+ expect(Object.keys(result.current.taskMetadata)).toHaveLength(1);
75
+
76
+ // Second fetch should reset
77
+ API.get.mockResolvedValue({ data: { 'task::two': {}, 'task::three': {} } });
78
+ await act(async () => {
79
+ await result.current.fetchTasks(2);
80
+ });
81
+ expect(Object.keys(result.current.taskMetadata)).toHaveLength(2);
82
+ });
83
+
84
+ test('fetchTasks handles errors and shows toast', async () => {
85
+ API.get.mockRejectedValue({ message: 'Network error' });
86
+
87
+ const { result } = renderHook(() => useTasksData(), { wrapper });
88
+
89
+ let returnValue;
90
+ await act(async () => {
91
+ returnValue = await result.current.fetchTasks(42);
92
+ });
93
+
94
+ expect(returnValue).toBeNull();
95
+ expect(result.current.isLoadingTasks).toBe(false);
96
+ expect(addToast).toHaveBeenCalledWith(
97
+ expect.objectContaining({
98
+ type: 'danger',
99
+ message: expect.stringContaining('Failed to load tasks'),
100
+ })
101
+ );
102
+ });
103
+ });
@@ -1,8 +1,8 @@
1
1
  import { useState, useCallback } from 'react';
2
- import { translate as __ } from 'foremanReact/common/I18n';
2
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
3
3
  import { API } from 'foremanReact/redux/API';
4
4
  import { ROUTES } from '../../common/constants';
5
- import { useShowMessage } from '../../common/helpers';
5
+ import { useShowMessage, extractErrorMessage } from '../../common/helpers';
6
6
 
7
7
  export const useOpenBoltOptions = () => {
8
8
  const showMessage = useShowMessage();
@@ -20,17 +20,10 @@ export const useOpenBoltOptions = () => {
20
20
  setOpenBoltOptions({});
21
21
 
22
22
  try {
23
- const { data, status } = await API.get(
23
+ const { data } = await API.get(
24
24
  `${ROUTES.API.FETCH_OPENBOLT_OPTIONS}?proxy_id=${proxyId}`
25
25
  );
26
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
27
  setOpenBoltOptionsMetadata(data || {});
35
28
 
36
29
  // Set defaults
@@ -44,7 +37,12 @@ export const useOpenBoltOptions = () => {
44
37
 
45
38
  return data;
46
39
  } catch (error) {
47
- showMessage(__('Failed to load OpenBolt options: ') + error.message);
40
+ showMessage(
41
+ sprintf(
42
+ __('Failed to load OpenBolt options: %s'),
43
+ extractErrorMessage(error)
44
+ )
45
+ );
48
46
  return null;
49
47
  } finally {
50
48
  setIsLoadingOptions(false);
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect } from 'react';
2
- import { translate as __ } from 'foremanReact/common/I18n';
2
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
3
3
  import { API } from 'foremanReact/redux/API';
4
- import { useShowMessage } from '../../common/helpers';
4
+ import { useShowMessage, extractErrorMessage } from '../../common/helpers';
5
5
 
6
6
  export const useSmartProxies = () => {
7
7
  const showMessage = useShowMessage();
@@ -16,18 +16,12 @@ export const useSmartProxies = () => {
16
16
  per_page: 'all',
17
17
  search: 'feature=OpenBolt',
18
18
  })}`;
19
- const { data, status } = await API.get(endpoint);
19
+ const { data } = await API.get(endpoint);
20
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 || []);
21
+ const results = data.results || [];
22
+ setSmartProxies(results);
29
23
 
30
- if (data.results.length === 0) {
24
+ if (results.length === 0) {
31
25
  showMessage(
32
26
  __(
33
27
  'No Smart Proxies found. Please check that one or more proxy has the smart_proxy_openbolt package installed and enabled.'
@@ -35,7 +29,12 @@ export const useSmartProxies = () => {
35
29
  );
36
30
  }
37
31
  } catch (error) {
38
- showMessage(__('Failed to load Smart Proxies: ') + error.message);
32
+ showMessage(
33
+ sprintf(
34
+ __('Failed to load Smart Proxies: %s'),
35
+ extractErrorMessage(error)
36
+ )
37
+ );
39
38
  } finally {
40
39
  setIsLoadingProxies(false);
41
40
  }
@@ -1,8 +1,8 @@
1
1
  import { useState, useCallback } from 'react';
2
- import { translate as __ } from 'foremanReact/common/I18n';
2
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
3
3
  import { API } from 'foremanReact/redux/API';
4
4
  import { ROUTES } from '../../common/constants';
5
- import { useShowMessage } from '../../common/helpers';
5
+ import { useShowMessage, extractErrorMessage } from '../../common/helpers';
6
6
 
7
7
  export const useTasksData = () => {
8
8
  const showMessage = useShowMessage();
@@ -24,16 +24,7 @@ export const useTasksData = () => {
24
24
  const endpoint = forceReload
25
25
  ? ROUTES.API.RELOAD_TASKS
26
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
- }
27
+ const { data } = await API.get(`${endpoint}?proxy_id=${proxyId}`);
37
28
 
38
29
  setTaskMetadata(data || {});
39
30
 
@@ -43,7 +34,9 @@ export const useTasksData = () => {
43
34
 
44
35
  return data;
45
36
  } catch (error) {
46
- showMessage(__('Failed to load tasks: ') + error.message);
37
+ showMessage(
38
+ sprintf(__('Failed to load tasks: %s'), extractErrorMessage(error))
39
+ );
47
40
  return null;
48
41
  } finally {
49
42
  setIsLoadingTasks(false);
@@ -1,7 +1,6 @@
1
- // TODO: More a11y tags
2
1
  import React, { useState, useCallback } from 'react';
3
2
  import { useHistory } from 'react-router-dom';
4
- import { translate as __ } from 'foremanReact/common/I18n';
3
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
4
 
6
5
  import { API } from 'foremanReact/redux/API';
7
6
  import {
@@ -28,7 +27,7 @@ import HostSelector from './HostSelector';
28
27
  import { useSmartProxies } from './hooks/useSmartProxies';
29
28
  import { useTasksData } from './hooks/useTasksData';
30
29
  import { useOpenBoltOptions } from './hooks/useOpenBoltOptions';
31
- import { useShowMessage } from '../common/helpers';
30
+ import { useShowMessage, extractErrorMessage } from '../common/helpers';
32
31
 
33
32
  const LaunchTask = () => {
34
33
  const history = useHistory();
@@ -96,7 +95,7 @@ const LaunchTask = () => {
96
95
  defaults[paramName] = paramMeta.default;
97
96
  } else if (
98
97
  ['boolean', 'optional[boolean]'].includes(
99
- paramMeta.type.toLowerCase()
98
+ paramMeta.type?.toLowerCase()
100
99
  )
101
100
  ) {
102
101
  defaults[paramName] = false;
@@ -174,36 +173,18 @@ const LaunchTask = () => {
174
173
  options: visibleOptions,
175
174
  };
176
175
 
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
- );
176
+ const { data } = await API.post(ROUTES.API.LAUNCH_TASK, body);
192
177
 
193
178
  history.push({
194
179
  pathname: ROUTES.PAGES.TASK_EXECUTION,
195
180
  search: new URLSearchParams({
196
- proxy_id: selectedProxy,
197
181
  job_id: data.job_id,
198
- proxy_name: selectedProxyData?.name || 'Unknown',
199
182
  }).toString(),
200
183
  });
201
184
  } catch (error) {
202
- const errorMessage =
203
- error.response?.data?.error ||
204
- error.message ||
205
- __('Unknown error occurred');
206
- showMessage(__('Failed to launch task: ') + errorMessage);
185
+ showMessage(
186
+ sprintf(__('Failed to launch task: %s'), extractErrorMessage(error))
187
+ );
207
188
  } finally {
208
189
  setIsSubmitting(false);
209
190
  }
@@ -265,7 +246,8 @@ const LaunchTask = () => {
265
246
  isDisabled={!isFormValid}
266
247
  isLoading={isSubmitting}
267
248
  >
268
- {`🚀 ${__('Launch Task')}`}
249
+ <span aria-hidden="true">🚀</span>
250
+ {` ${__('Launch Task')}`}
269
251
  </Button>
270
252
  </FlexItem>
271
253
  </Flex>