foreman_remote_execution 9.0.1 → 10.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +14 -0
  3. data/.tx/config +3 -1
  4. data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +1 -0
  5. data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +1 -0
  6. data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +1 -0
  7. data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +1 -0
  8. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +1 -0
  9. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +1 -0
  10. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +1 -0
  11. data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +1 -0
  12. data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +1 -0
  13. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +1 -0
  14. data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +1 -0
  15. data/app/assets/javascripts/foreman_remote_execution/template_invocation.js +10 -1
  16. data/app/controllers/api/v2/job_invocations_controller.rb +1 -0
  17. data/app/controllers/job_invocations_controller.rb +30 -1
  18. data/app/controllers/ui_job_wizard_controller.rb +3 -1
  19. data/app/helpers/remote_execution_helper.rb +1 -1
  20. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  21. data/app/lib/actions/remote_execution/run_hosts_job.rb +28 -2
  22. data/app/models/remote_execution_feature.rb +11 -8
  23. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  24. data/app/views/job_invocations/_form.html.erb +1 -1
  25. data/app/views/job_invocations/show.html.erb +1 -1
  26. data/app/views/job_invocations/welcome.html.erb +1 -1
  27. data/app/views/templates/script/run_command.erb +1 -0
  28. data/config/routes.rb +2 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +1 -1
  30. data/db/migrate/20220426145007_add_unique_feature_label_index.rb +14 -0
  31. data/lib/foreman_remote_execution/engine.rb +9 -9
  32. data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +12 -3
  33. data/lib/foreman_remote_execution/version.rb +1 -1
  34. data/locale/Makefile +6 -3
  35. data/locale/action_names.rb +1 -1
  36. data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  37. data/locale/de/foreman_remote_execution.po +67 -19
  38. data/locale/en/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  39. data/locale/en/foreman_remote_execution.po +56 -8
  40. data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  41. data/locale/en_GB/foreman_remote_execution.po +58 -10
  42. data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  43. data/locale/es/foreman_remote_execution.po +71 -23
  44. data/locale/foreman_remote_execution.pot +216 -141
  45. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  46. data/locale/fr/foreman_remote_execution.po +104 -56
  47. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  48. data/locale/ja/foreman_remote_execution.po +72 -24
  49. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  50. data/locale/ko/foreman_remote_execution.po +63 -15
  51. data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  52. data/locale/pt_BR/foreman_remote_execution.po +68 -20
  53. data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  54. data/locale/ru/foreman_remote_execution.po +63 -15
  55. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  56. data/locale/zh_CN/foreman_remote_execution.po +71 -23
  57. data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  58. data/locale/zh_TW/foreman_remote_execution.po +63 -15
  59. data/package.json +6 -6
  60. data/webpack/JobWizard/Footer.js +104 -0
  61. data/webpack/JobWizard/JobWizard.js +105 -32
  62. data/webpack/JobWizard/JobWizard.scss +6 -17
  63. data/webpack/JobWizard/JobWizardConstants.js +1 -1
  64. data/webpack/JobWizard/JobWizardPageRerun.js +2 -0
  65. data/webpack/JobWizard/StartsBeforeErrorAlert.js +17 -0
  66. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +8 -0
  67. data/webpack/JobWizard/__tests__/fixtures.js +13 -1
  68. data/webpack/JobWizard/__tests__/integration.test.js +15 -0
  69. data/webpack/JobWizard/__tests__/validation.test.js +36 -0
  70. data/webpack/JobWizard/autofill.js +1 -0
  71. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -10
  72. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +8 -0
  73. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +5 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +28 -14
  75. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +41 -4
  76. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +16 -10
  77. data/webpack/JobWizard/steps/HostsAndInputs/index.js +51 -3
  78. data/webpack/JobWizard/steps/ReviewDetails/ReviewDetails.test.js +117 -0
  79. data/webpack/JobWizard/steps/ReviewDetails/helpers.js +43 -0
  80. data/webpack/JobWizard/steps/ReviewDetails/index.js +169 -18
  81. data/webpack/JobWizard/steps/Schedule/QueryType.js +1 -1
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +0 -1
  83. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +1 -1
  84. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +52 -18
  85. data/webpack/JobWizard/steps/form/DateTimePicker.js +1 -2
  86. data/webpack/JobWizard/steps/form/GroupedSelectField.js +0 -1
  87. data/webpack/JobWizard/steps/form/SelectField.js +0 -1
  88. data/webpack/JobWizard/submit.js +14 -3
  89. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +4 -4
  90. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +2 -2
  91. data/webpack/react_app/components/RecentJobsCard/constants.js +2 -2
  92. data/webpack/react_app/components/RegistrationExtension/RexPull.js +0 -2
  93. metadata +23 -6
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Chip, ChipGroup, Button } from '@patternfly/react-core';
4
- import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
5
  import { hostMethods } from '../../JobWizardConstants';
6
6
 
7
7
  const SelectedChip = ({ selected, setSelected, categoryName }) => {
@@ -10,19 +10,33 @@ const SelectedChip = ({ selected, setSelected, categoryName }) => {
10
10
  oldSelected.filter(({ id }) => id !== itemToRemove)
11
11
  );
12
12
  };
13
+ const NUM_CHIPS = 3;
13
14
  return (
14
- <ChipGroup className="hosts-chip-group" categoryName={categoryName}>
15
- {selected.map(({ name, id }, index) => (
16
- <Chip
17
- key={index}
18
- id={`${categoryName}-${id}`}
19
- onClick={() => deleteItem(id)}
20
- closeBtnAriaLabel={`Close ${name}`}
21
- >
22
- {name}
23
- </Chip>
24
- ))}
25
- </ChipGroup>
15
+ <>
16
+ <ChipGroup
17
+ className="hosts-chip-group"
18
+ categoryName={categoryName}
19
+ isClosable
20
+ closeBtnAriaLabel="Remove all"
21
+ collapsedText={sprintf(__('%s more'), selected.length - NUM_CHIPS)}
22
+ numChips={NUM_CHIPS}
23
+ onClick={() => {
24
+ setSelected(() => []);
25
+ }}
26
+ >
27
+ {selected.map(({ name, id }, index) => (
28
+ <Chip
29
+ key={index}
30
+ id={`${categoryName}-${id}`}
31
+ onClick={() => deleteItem(id)}
32
+ closeBtnAriaLabel={`Remove ${name}`}
33
+ >
34
+ {name}
35
+ </Chip>
36
+ ))}
37
+ </ChipGroup>
38
+ {selected.length > 0 && <br />}
39
+ </>
26
40
  );
27
41
  };
28
42
 
@@ -75,7 +89,7 @@ export const SelectedChips = ({
75
89
  />
76
90
  {showClear && (
77
91
  <Button variant="link" className="clear-chips" onClick={clearAll}>
78
- {__('Clear filters')}
92
+ {__('Clear all filters')}
79
93
  </Button>
80
94
  )}
81
95
  </div>
@@ -30,6 +30,27 @@ describe('Hosts', () => {
30
30
  const select = name =>
31
31
  screen.getByRole('button', { name: `${name} toggle` });
32
32
  fireEvent.click(select('hosts'));
33
+ await act(async () => {
34
+ fireEvent.click(screen.getByText('host1'));
35
+ fireEvent.click(select('hosts'));
36
+ });
37
+ expect(
38
+ screen.queryAllByText('Please select at least one host')
39
+ ).toHaveLength(0);
40
+ await act(async () => {
41
+ fireEvent.click(select('hosts'));
42
+ });
43
+ await act(async () => {
44
+ fireEvent.click(
45
+ screen.getByText('host1', {
46
+ selector: '.pf-c-select__menu-item',
47
+ })
48
+ );
49
+ fireEvent.blur(select('hosts'));
50
+ });
51
+ expect(
52
+ screen.queryAllByText('Please select at least one host')
53
+ ).toHaveLength(1);
33
54
  await act(async () => {
34
55
  fireEvent.click(screen.getByText('host1'));
35
56
  fireEvent.click(screen.getByText('host2'));
@@ -59,7 +80,7 @@ describe('Hosts', () => {
59
80
  expect(screen.queryAllByText('host1')).toHaveLength(1);
60
81
  expect(screen.queryAllByText('host2')).toHaveLength(1);
61
82
  expect(screen.queryAllByText('host3')).toHaveLength(0);
62
- const chip1 = screen.getByRole('button', { name: 'Close host1 host1' });
83
+ const chip1 = screen.getByRole('button', { name: 'Remove host1 host1' });
63
84
  await act(async () => {
64
85
  fireEvent.click(chip1);
65
86
  });
@@ -70,7 +91,7 @@ describe('Hosts', () => {
70
91
  expect(screen.queryAllByText('host_collection1')).toHaveLength(1);
71
92
 
72
93
  await act(async () => {
73
- fireEvent.click(screen.getByText('Category and Template'));
94
+ fireEvent.click(screen.getByText('Category and template'));
74
95
  });
75
96
  await act(async () => {
76
97
  fireEvent.click(screen.getByText('Target hosts and inputs'));
@@ -79,7 +100,7 @@ describe('Hosts', () => {
79
100
  expect(screen.queryAllByText('host_group1')).toHaveLength(1);
80
101
 
81
102
  await act(async () => {
82
- fireEvent.click(screen.getByText('Clear filters'));
103
+ fireEvent.click(screen.getByText('Clear all filters'));
83
104
  });
84
105
 
85
106
  expect(screen.queryAllByText('host2')).toHaveLength(0);
@@ -107,6 +128,13 @@ describe('Hosts', () => {
107
128
  expect(screen.queryAllByText('Host groups')).toHaveLength(1);
108
129
  expect(screen.queryAllByText('Search query')).toHaveLength(1);
109
130
  expect(screen.queryAllByText('Host collections')).toHaveLength(0);
131
+
132
+ await act(async () => {
133
+ fireEvent.click(
134
+ // Close the select
135
+ screen.getByText('Hosts', { selector: '.pf-c-select__toggle-text' })
136
+ );
137
+ });
110
138
  });
111
139
  it('Host fill list from url', async () => {
112
140
  routerSelectors.selectRouterLocation.mockImplementation(() => ({
@@ -151,8 +179,9 @@ describe('Hosts', () => {
151
179
 
152
180
  it('input fill from url', async () => {
153
181
  const inputText = 'test text';
182
+ const advancedInputText = 'test adv text';
154
183
  routerSelectors.selectRouterLocation.mockImplementation(() => ({
155
- search: `feature=test_feature&inputs[plain hidden]=${inputText}`,
184
+ search: `host_ids%5B%5D=host1&host_ids%5B%5D=host3&feature=test_feature&inputs[plain hidden]=${inputText}&inputs[adv plain hidden]=${advancedInputText}`,
156
185
  }));
157
186
  render(
158
187
  <MockedProvider mocks={gqlMock} addTypename={false}>
@@ -175,5 +204,13 @@ describe('Hosts', () => {
175
204
  selector: 'textarea',
176
205
  });
177
206
  expect(textField.value).toBe(inputText);
207
+
208
+ await act(async () => {
209
+ fireEvent.click(screen.getByText('Advanced fields'));
210
+ });
211
+ const advancedTextField = screen.getByLabelText('adv plain hidden', {
212
+ selector: 'textarea',
213
+ });
214
+ expect(advancedTextField.value).toBe(advancedInputText);
178
215
  });
179
216
  });
@@ -1,18 +1,24 @@
1
1
  export const buildHostQuery = (selected, search) => {
2
2
  const { hosts, hostCollections, hostGroups } = selected;
3
- const hostsSearch = `(id ^ (${hosts.map(({ id }) => id).join(',')}))`;
4
- const hostCollectionsSearch = `(host_collection_id ^ (${hostCollections
3
+ const hostsSearch = `id ^ (${hosts.map(({ id }) => id).join(',')})`;
4
+ const hostCollectionsSearch = `host_collection_id ^ (${hostCollections
5
5
  .map(({ id }) => id)
6
- .join(',')}))`;
7
- const hostGroupsSearch = `(hostgroup_id ^ (${hostGroups
6
+ .join(',')})`;
7
+ const hostGroupsSearch = `hostgroup_id ^ (${hostGroups
8
8
  .map(({ id }) => id)
9
- .join(',')}))`;
10
- return [
9
+ .join(',')})`;
10
+ const queryParts = [
11
11
  hosts.length ? hostsSearch : false,
12
12
  hostCollections.length ? hostCollectionsSearch : false,
13
13
  hostGroups.length ? hostGroupsSearch : false,
14
- search.length ? `(${search})` : false,
15
- ]
16
- .filter(Boolean)
17
- .join(' or ');
14
+ search.length ? search : false,
15
+ ].filter(Boolean);
16
+
17
+ if (queryParts.length === 0) {
18
+ return 'name=a AND name=b';
19
+ }
20
+ if (queryParts.length === 1) {
21
+ return queryParts[0] || 'name=a AND name=b';
22
+ }
23
+ return queryParts.map(p => `(${p})`).join(' or ') || 'name=a AND name=b';
18
24
  };
@@ -51,6 +51,30 @@ const HostsAndInputs = ({
51
51
  const isLoading = useSelector(selectIsLoadingHosts);
52
52
  const templateInputs = useSelector(selectTemplateInputs);
53
53
  const [hostPreviewOpen, setHostPreviewOpen] = useState(false);
54
+ const [wasFocus, setWasFocus] = useState(false);
55
+ const [isError, setIsError] = useState(false);
56
+ useEffect(() => {
57
+ if (wasFocus) {
58
+ if (
59
+ selected.hosts.length === 0 &&
60
+ selected.hostCollections.length === 0 &&
61
+ selected.hostGroups.length === 0 &&
62
+ hostsSearchQuery.length === 0
63
+ ) {
64
+ setIsError(true);
65
+ } else {
66
+ setIsError(false);
67
+ }
68
+ }
69
+ }, [
70
+ hostMethod,
71
+ hostsSearchQuery.length,
72
+ selected,
73
+ selected.hostCollections.length,
74
+ selected.hostGroups.length,
75
+ selected.hosts.length,
76
+ wasFocus,
77
+ ]);
54
78
  useEffect(() => {
55
79
  debounce(() => {
56
80
  dispatch(
@@ -99,6 +123,9 @@ const HostsAndInputs = ({
99
123
  const clearSearch = () => {
100
124
  setHostsSearchQuery('');
101
125
  };
126
+ const [errorText, setErrorText] = useState(
127
+ __('Please select at least one host')
128
+ );
102
129
  return (
103
130
  <div className="target-hosts-and-inputs">
104
131
  <WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
@@ -110,8 +137,13 @@ const HostsAndInputs = ({
110
137
  />
111
138
  )}
112
139
  <Form>
113
- <FormGroup fieldId="host_selection" id="host-selection">
114
- <InputGroup>
140
+ <FormGroup
141
+ fieldId="host_selection"
142
+ id="host-selection"
143
+ helperTextInvalid={errorText}
144
+ validated={isError ? 'error' : 'default'}
145
+ >
146
+ <InputGroup onBlur={() => setWasFocus(true)}>
115
147
  <SelectField
116
148
  isRequired
117
149
  className="target-method-select"
@@ -123,7 +155,23 @@ const HostsAndInputs = ({
123
155
  }
124
156
  return true;
125
157
  })}
126
- setValue={setHostMethod}
158
+ setValue={val => {
159
+ setHostMethod(val);
160
+ if (val === hostMethods.searchQuery) {
161
+ setErrorText(__('Please enter a search query'));
162
+ }
163
+ if (val === hostMethods.hosts) {
164
+ setErrorText(__('Please select at least one host'));
165
+ }
166
+ if (val === hostMethods.hostCollections) {
167
+ setErrorText(
168
+ __('Please select at least one host collection')
169
+ );
170
+ }
171
+ if (val === hostMethods.hostGroups) {
172
+ setErrorText(__('Please select at least one host group'));
173
+ }
174
+ }}
127
175
  value={hostMethod}
128
176
  />
129
177
  {hostMethod === hostMethods.searchQuery && (
@@ -0,0 +1,117 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { render, fireEvent, screen, act } from '@testing-library/react';
4
+ import { MockedProvider } from '@apollo/client/testing';
5
+ import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
6
+ import * as api from 'foremanReact/redux/API';
7
+
8
+ import JobWizardPageRerun from '../../JobWizardPageRerun';
9
+ import * as selectors from '../../JobWizardSelectors';
10
+ import {
11
+ testSetup,
12
+ mockApi,
13
+ gqlMock,
14
+ jobInvocation,
15
+ } from '../../__tests__/fixtures';
16
+
17
+ const store = testSetup(selectors, api);
18
+ mockApi(api);
19
+ jest.spyOn(APIHooks, 'useAPI');
20
+ APIHooks.useAPI.mockImplementation((action, url) => {
21
+ if (url === '/ui_job_wizard/job_invocation?id=57') {
22
+ return { response: jobInvocation, status: 'RESOLVED' };
23
+ }
24
+ return {};
25
+ });
26
+ jest.useFakeTimers();
27
+
28
+ describe('ReviewDetails', () => {
29
+ it('should call goToStepByName function when StepButton is clicked', async () => {
30
+ render(
31
+ <MockedProvider mocks={gqlMock} addTypename={false}>
32
+ <Provider store={store}>
33
+ <JobWizardPageRerun
34
+ match={{
35
+ params: { id: '57' },
36
+ }}
37
+ />
38
+ </Provider>
39
+ </MockedProvider>
40
+ );
41
+
42
+ act(() => {
43
+ fireEvent.click(screen.getByText('Type of execution'));
44
+ });
45
+ act(() => {
46
+ fireEvent.click(screen.getByText('Future execution'));
47
+
48
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover useTimer
49
+ });
50
+ act(() => {
51
+ fireEvent.click(screen.getByRole('button', { name: 'Future execution' }));
52
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover useTimer
53
+ });
54
+
55
+ const newStartAtDate = '2030/03/12';
56
+ const newStartAtTime = '14:27';
57
+ const startsAtDateField = () =>
58
+ screen.getByLabelText('starts at datepicker');
59
+ const startsAtTimeField = () =>
60
+ screen.getByLabelText('starts at timepicker');
61
+
62
+ await act(async () => {
63
+ await fireEvent.change(startsAtDateField(), {
64
+ target: { value: newStartAtDate },
65
+ });
66
+ fireEvent.change(startsAtTimeField(), {
67
+ target: { value: newStartAtTime },
68
+ });
69
+ jest.advanceTimersByTime(1000);
70
+ });
71
+
72
+ act(() => {
73
+ fireEvent.click(screen.getByText('Review details'));
74
+ });
75
+ expect(screen.getAllByText('Review details')).toHaveLength(3);
76
+ fireEvent.click(
77
+ screen.getByText('Job template', {
78
+ selector: '.pf-c-button',
79
+ })
80
+ );
81
+ expect(screen.getAllByText('Category and template')).toHaveLength(3);
82
+
83
+ await act(async () => {
84
+ fireEvent.click(screen.getByText('Review details'));
85
+ jest.advanceTimersByTime(1000);
86
+ });
87
+ act(() => {
88
+ fireEvent.click(
89
+ screen.getByText('Target hosts', {
90
+ selector: '.pf-c-button',
91
+ })
92
+ );
93
+ jest.advanceTimersByTime(1000); // to handle pf4 date picker popover useTimer
94
+ });
95
+ expect(screen.getAllByText('Target hosts and inputs')).toHaveLength(3);
96
+ act(() => {
97
+ fireEvent.click(screen.getByText('Review details'));
98
+ });
99
+ act(() => {
100
+ fireEvent.click(
101
+ screen.getByText('Advanced fields', {
102
+ selector: '.pf-c-button',
103
+ })
104
+ );
105
+ jest.advanceTimersByTime(1000);
106
+ });
107
+ expect(screen.getAllByText('Advanced fields')).toHaveLength(3);
108
+
109
+ act(() => {
110
+ fireEvent.click(screen.getByText('Review details'));
111
+ });
112
+ act(() => {
113
+ fireEvent.click(screen.getByText('Recurrence'));
114
+ });
115
+ expect(screen.getAllByText('Schedule')).toHaveLength(3);
116
+ });
117
+ });
@@ -0,0 +1,43 @@
1
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
2
+ import { getWeekDays } from '../Schedule/RepeatWeek';
3
+ import { repeatTypes } from '../../JobWizardConstants';
4
+
5
+ export const parseEnd = (ends, isNeverEnds, repeatAmount) => {
6
+ if (isNeverEnds) {
7
+ return __('Never');
8
+ }
9
+ if (ends) {
10
+ const endsDate = new Date(ends);
11
+ return endsDate.toString();
12
+ }
13
+ return sprintf(__('After %s occurences'), repeatAmount);
14
+ };
15
+
16
+ export const parseRepeat = (repeatType, repeatData) => {
17
+ switch (repeatType) {
18
+ case repeatTypes.hourly:
19
+ return sprintf(__('Every hour at minute %s'), repeatData.minute);
20
+ case repeatTypes.daily:
21
+ return sprintf(__('Every day at %s'), repeatData.at);
22
+ case repeatTypes.weekly: {
23
+ const daysKeys = Object.keys(repeatData.daysOfWeek).filter(
24
+ k => repeatData.daysOfWeek[k]
25
+ );
26
+ const days = getWeekDays()
27
+ .filter((d, index) => index in daysKeys)
28
+ .join(', ');
29
+ return sprintf(__('Every week on %s at %s'), days, repeatData.at);
30
+ }
31
+ case repeatTypes.monthly:
32
+ return sprintf(
33
+ __('Every month on %s at %s'),
34
+ repeatData.days,
35
+ repeatData.at
36
+ );
37
+ case repeatTypes.cronline:
38
+ return `${__('Cron line')} - ${repeatData.cronline}`;
39
+
40
+ default:
41
+ return '';
42
+ }
43
+ };