foreman_remote_execution 4.5.5 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_ci.yml +7 -0
- data/.rubocop_todo.yml +1 -0
- data/app/controllers/api/v2/job_invocations_controller.rb +7 -1
- data/app/graphql/types/job_invocation.rb +16 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +2 -1
- data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
- data/app/mailers/rex_job_mailer.rb +15 -0
- data/app/models/job_invocation.rb +4 -0
- data/app/models/job_invocation_composer.rb +21 -13
- data/app/models/job_template.rb +1 -1
- data/app/models/remote_execution_provider.rb +17 -2
- data/app/models/rex_mail_notification.rb +13 -0
- data/app/models/setting/remote_execution.rb +7 -1
- data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
- data/app/views/dashboard/_latest-jobs.html.erb +21 -0
- data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
- data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
- data/app/views/template_invocations/show.html.erb +2 -1
- data/db/seeds.d/50-notification_blueprints.rb +14 -0
- data/db/seeds.d/95-mail_notifications.rb +24 -0
- data/foreman_remote_execution.gemspec +2 -4
- data/lib/foreman_remote_execution/engine.rb +4 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +6 -6
- data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
- data/test/graphql/queries/job_invocation_query_test.rb +31 -0
- data/test/graphql/queries/job_invocations_query_test.rb +35 -0
- data/test/unit/actions/run_hosts_job_test.rb +99 -4
- data/test/unit/concerns/host_extensions_test.rb +4 -4
- data/test/unit/input_template_renderer_test.rb +1 -89
- data/test/unit/job_invocation_composer_test.rb +1 -12
- data/test/unit/job_invocation_report_template_test.rb +15 -12
- data/test/unit/remote_execution_provider_test.rb +34 -0
- data/webpack/JobWizard/JobWizard.js +53 -20
- data/webpack/JobWizard/JobWizard.scss +33 -4
- data/webpack/JobWizard/JobWizardConstants.js +17 -0
- data/webpack/JobWizard/__tests__/fixtures.js +8 -0
- data/webpack/JobWizard/__tests__/integration.test.js +3 -7
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +16 -5
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +48 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +29 -14
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +3 -2
- data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +25 -0
- data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +37 -0
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +50 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +66 -0
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
- data/webpack/JobWizard/steps/Schedule/StartEndDates.js +36 -21
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +155 -0
- data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +9 -8
- data/webpack/JobWizard/steps/Schedule/index.js +89 -28
- data/webpack/JobWizard/steps/form/DateTimePicker.js +93 -0
- data/webpack/JobWizard/steps/form/Formatter.js +10 -9
- data/webpack/JobWizard/steps/form/NumberInput.js +2 -0
- data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
- data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
- data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
- data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
- data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
- metadata +26 -19
- data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -10,24 +10,16 @@ import {
|
|
10
10
|
testSetup,
|
11
11
|
mockApi,
|
12
12
|
} from '../../../__tests__/fixtures';
|
13
|
+
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
13
14
|
|
14
15
|
const store = testSetup(selectors, api);
|
15
16
|
mockApi(api);
|
16
17
|
|
17
18
|
jest.spyOn(selectors, 'selectEffectiveUser');
|
18
|
-
jest.spyOn(selectors, 'selectTemplateInputs');
|
19
|
-
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
20
19
|
|
21
20
|
selectors.selectEffectiveUser.mockImplementation(
|
22
21
|
() => jobTemplate.effective_user
|
23
22
|
);
|
24
|
-
selectors.selectTemplateInputs.mockImplementation(
|
25
|
-
() => jobTemplate.template_inputs
|
26
|
-
);
|
27
|
-
|
28
|
-
selectors.selectAdvancedTemplateInputs.mockImplementation(
|
29
|
-
() => jobTemplate.advanced_template_inputs
|
30
|
-
);
|
31
23
|
describe('AdvancedFields', () => {
|
32
24
|
it('should save data between steps for advanced fields', async () => {
|
33
25
|
const wrapper = mount(
|
@@ -72,7 +64,7 @@ describe('AdvancedFields', () => {
|
|
72
64
|
.simulate('click');
|
73
65
|
|
74
66
|
expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
|
75
|
-
'Target
|
67
|
+
'Target hosts and inputs'
|
76
68
|
);
|
77
69
|
wrapper
|
78
70
|
.find('.pf-c-wizard__nav-link')
|
@@ -91,7 +83,7 @@ describe('AdvancedFields', () => {
|
|
91
83
|
</Provider>
|
92
84
|
);
|
93
85
|
await act(async () => {
|
94
|
-
fireEvent.click(screen.getByText(
|
86
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
95
87
|
});
|
96
88
|
const searchValue = 'search test';
|
97
89
|
const textValue = 'I am a text';
|
@@ -108,7 +100,7 @@ describe('AdvancedFields', () => {
|
|
108
100
|
fireEvent.click(selectField);
|
109
101
|
await act(async () => {
|
110
102
|
await fireEvent.click(screen.getByText('option 2'));
|
111
|
-
fireEvent.click(screen.getAllByText(
|
103
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
|
112
104
|
await fireEvent.change(textField, {
|
113
105
|
target: { value: textValue },
|
114
106
|
});
|
@@ -128,9 +120,11 @@ describe('AdvancedFields', () => {
|
|
128
120
|
expect(searchField.value).toBe(searchValue);
|
129
121
|
expect(dateField.value).toBe(dateValue);
|
130
122
|
await act(async () => {
|
131
|
-
fireEvent.click(screen.getByText(
|
123
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
132
124
|
});
|
133
|
-
expect(screen.getAllByText(
|
125
|
+
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
|
126
|
+
3
|
127
|
+
);
|
134
128
|
|
135
129
|
await act(async () => {
|
136
130
|
fireEvent.click(screen.getByText('Advanced Fields'));
|
@@ -141,4 +135,25 @@ describe('AdvancedFields', () => {
|
|
141
135
|
expect(screen.queryAllByText('option 1')).toHaveLength(0);
|
142
136
|
expect(screen.queryAllByText('option 2')).toHaveLength(1);
|
143
137
|
});
|
138
|
+
it('fill defaults into fields', async () => {
|
139
|
+
render(
|
140
|
+
<Provider store={store}>
|
141
|
+
<JobWizard />
|
142
|
+
</Provider>
|
143
|
+
);
|
144
|
+
await act(async () => {
|
145
|
+
fireEvent.click(screen.getByText('Advanced Fields'));
|
146
|
+
});
|
147
|
+
|
148
|
+
expect(
|
149
|
+
screen.getByLabelText('effective user', {
|
150
|
+
selector: 'input',
|
151
|
+
}).value
|
152
|
+
).toBe('default effective user');
|
153
|
+
expect(
|
154
|
+
screen.getByLabelText('timeout to kill', {
|
155
|
+
selector: 'input',
|
156
|
+
}).value
|
157
|
+
).toBe('2');
|
158
|
+
});
|
144
159
|
});
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
|
-
import {
|
3
|
+
import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
5
5
|
import { SelectField } from '../form/SelectField';
|
6
6
|
import { GroupedSelectField } from '../form/GroupedSelectField';
|
7
|
+
import { WizardTitle } from '../form/WizardTitle';
|
8
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
7
9
|
|
8
10
|
export const CategoryAndTemplate = ({
|
9
11
|
jobCategories,
|
@@ -40,7 +42,7 @@ export const CategoryAndTemplate = ({
|
|
40
42
|
const isError = !!(categoryError || allTemplatesError || templateError);
|
41
43
|
return (
|
42
44
|
<>
|
43
|
-
<
|
45
|
+
<WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
|
44
46
|
<Text component={TextVariants.p}>{__('All fields are required.')}</Text>
|
45
47
|
<Form>
|
46
48
|
<SelectField
|
@@ -5,6 +5,7 @@ import * as api from 'foremanReact/redux/API';
|
|
5
5
|
import { JobWizard } from '../../JobWizard';
|
6
6
|
import * as selectors from '../../JobWizardSelectors';
|
7
7
|
import { testSetup, mockApi } from '../../__tests__/fixtures';
|
8
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
8
9
|
|
9
10
|
const store = testSetup(selectors, api);
|
10
11
|
mockApi(api);
|
@@ -32,7 +33,7 @@ describe('Category And Template', () => {
|
|
32
33
|
await act(async () => {
|
33
34
|
await fireEvent.click(screen.getByText('Puppet'));
|
34
35
|
});
|
35
|
-
fireEvent.click(screen.getAllByText(
|
36
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
|
36
37
|
expect(
|
37
38
|
screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
|
38
39
|
).toHaveLength(0);
|
@@ -47,7 +48,7 @@ describe('Category And Template', () => {
|
|
47
48
|
await act(async () => {
|
48
49
|
await fireEvent.click(screen.getByText('template2'));
|
49
50
|
});
|
50
|
-
fireEvent.click(screen.getAllByText(
|
51
|
+
fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
|
51
52
|
expect(
|
52
53
|
screen.queryAllByDisplayValue('template1', { selector: 'button' })
|
53
54
|
).toHaveLength(0);
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Chip, ChipGroup } from '@patternfly/react-core';
|
4
|
+
|
5
|
+
export const SelectedChips = ({ selected, setSelected }) => {
|
6
|
+
const deleteItem = itemToRemove => {
|
7
|
+
setSelected(oldSelected =>
|
8
|
+
oldSelected.filter(item => item !== itemToRemove)
|
9
|
+
);
|
10
|
+
};
|
11
|
+
return (
|
12
|
+
<ChipGroup className="hosts-chip-group">
|
13
|
+
{selected.map(chip => (
|
14
|
+
<Chip key={chip} id={chip} onClick={() => deleteItem(chip)}>
|
15
|
+
{chip}
|
16
|
+
</Chip>
|
17
|
+
))}
|
18
|
+
</ChipGroup>
|
19
|
+
);
|
20
|
+
};
|
21
|
+
|
22
|
+
SelectedChips.propTypes = {
|
23
|
+
selected: PropTypes.array.isRequired,
|
24
|
+
setSelected: PropTypes.func.isRequired,
|
25
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { formatter } from '../form/Formatter';
|
5
|
+
|
6
|
+
export const TemplateInputs = ({ inputs, value, setValue }) => {
|
7
|
+
if (inputs.length)
|
8
|
+
return inputs.map(input => formatter(input, value, setValue));
|
9
|
+
return (
|
10
|
+
<p className="gray-text">
|
11
|
+
{__('There are no available input fields for the selected template.')}
|
12
|
+
</p>
|
13
|
+
);
|
14
|
+
};
|
15
|
+
TemplateInputs.propTypes = {
|
16
|
+
inputs: PropTypes.array.isRequired,
|
17
|
+
value: PropTypes.object,
|
18
|
+
setValue: PropTypes.func.isRequired,
|
19
|
+
};
|
20
|
+
|
21
|
+
TemplateInputs.defaultProps = {
|
22
|
+
value: {},
|
23
|
+
};
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import { fireEvent, screen, render, act } from '@testing-library/react';
|
4
|
+
import * as api from 'foremanReact/redux/API';
|
5
|
+
import { JobWizard } from '../../../JobWizard';
|
6
|
+
import * as selectors from '../../../JobWizardSelectors';
|
7
|
+
import { testSetup, mockApi } from '../../../__tests__/fixtures';
|
8
|
+
|
9
|
+
const store = testSetup(selectors, api);
|
10
|
+
mockApi(api);
|
11
|
+
|
12
|
+
describe('TemplateInputs', () => {
|
13
|
+
it('should save data between steps for template input fields', async () => {
|
14
|
+
render(
|
15
|
+
<Provider store={store}>
|
16
|
+
<JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
|
17
|
+
</Provider>
|
18
|
+
);
|
19
|
+
await act(async () => {
|
20
|
+
await fireEvent.click(
|
21
|
+
screen.getByText('Target hosts and inputs', { selector: 'button' })
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
expect(
|
26
|
+
screen.getAllByLabelText('host2', { selector: 'button' })
|
27
|
+
).toHaveLength(1);
|
28
|
+
const chip1 = screen.getByLabelText('host1', { selector: 'button' });
|
29
|
+
fireEvent.click(chip1);
|
30
|
+
expect(
|
31
|
+
screen.queryAllByLabelText('host1', { selector: 'button' })
|
32
|
+
).toHaveLength(0);
|
33
|
+
expect(
|
34
|
+
screen.queryAllByLabelText('host2', { selector: 'button' })
|
35
|
+
).toHaveLength(1);
|
36
|
+
});
|
37
|
+
});
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Provider } from 'react-redux';
|
3
|
+
import { fireEvent, screen, render, act } from '@testing-library/react';
|
4
|
+
import * as api from 'foremanReact/redux/API';
|
5
|
+
import { JobWizard } from '../../../JobWizard';
|
6
|
+
import * as selectors from '../../../JobWizardSelectors';
|
7
|
+
import { testSetup, mockApi } from '../../../__tests__/fixtures';
|
8
|
+
import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
9
|
+
|
10
|
+
const store = testSetup(selectors, api);
|
11
|
+
mockApi(api);
|
12
|
+
|
13
|
+
describe('TemplateInputs', () => {
|
14
|
+
it('should save data between steps for template input fields', async () => {
|
15
|
+
render(
|
16
|
+
<Provider store={store}>
|
17
|
+
<JobWizard />
|
18
|
+
</Provider>
|
19
|
+
);
|
20
|
+
await act(async () => {
|
21
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
|
22
|
+
});
|
23
|
+
const textValue = 'I am a plain text';
|
24
|
+
const textField = screen.getByLabelText('plain hidden', {
|
25
|
+
selector: 'textarea',
|
26
|
+
});
|
27
|
+
|
28
|
+
await act(async () => {
|
29
|
+
await fireEvent.change(textField, {
|
30
|
+
target: { value: textValue },
|
31
|
+
});
|
32
|
+
});
|
33
|
+
expect(
|
34
|
+
screen.getByLabelText('plain hidden', {
|
35
|
+
selector: 'textarea',
|
36
|
+
}).value
|
37
|
+
).toBe(textValue);
|
38
|
+
await act(async () => {
|
39
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
40
|
+
});
|
41
|
+
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
|
42
|
+
3
|
43
|
+
);
|
44
|
+
|
45
|
+
await act(async () => {
|
46
|
+
fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
|
47
|
+
});
|
48
|
+
expect(textField.value).toBe(textValue);
|
49
|
+
});
|
50
|
+
});
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Button, Form, FormGroup } from '@patternfly/react-core';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
import { useSelector } from 'react-redux';
|
5
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
6
|
+
import { selectTemplateInputs } from '../../JobWizardSelectors';
|
7
|
+
import { SelectField } from '../form/SelectField';
|
8
|
+
import { SelectedChips } from './SelectedChips';
|
9
|
+
import { TemplateInputs } from './TemplateInputs';
|
10
|
+
import { WIZARD_TITLES } from '../../JobWizardConstants';
|
11
|
+
import { WizardTitle } from '../form/WizardTitle';
|
12
|
+
|
13
|
+
const HostsAndInputs = ({
|
14
|
+
templateValues,
|
15
|
+
setTemplateValues,
|
16
|
+
selectedHosts,
|
17
|
+
setSelectedHosts,
|
18
|
+
}) => {
|
19
|
+
const templateInputs = useSelector(selectTemplateInputs);
|
20
|
+
const hostMethods = [
|
21
|
+
__('Hosts'),
|
22
|
+
__('Host collection'),
|
23
|
+
__('Host group'),
|
24
|
+
__('Search query'),
|
25
|
+
];
|
26
|
+
const [hostMethod, setHostMethod] = useState(hostMethods[0]);
|
27
|
+
return (
|
28
|
+
<>
|
29
|
+
<WizardTitle title={WIZARD_TITLES.hostsAndInputs} />
|
30
|
+
<Form>
|
31
|
+
<FormGroup fieldId="host_selection">
|
32
|
+
<SelectField
|
33
|
+
fieldId="host_methods"
|
34
|
+
options={hostMethods}
|
35
|
+
setValue={setHostMethod}
|
36
|
+
value={hostMethod}
|
37
|
+
/>
|
38
|
+
<SelectedChips
|
39
|
+
selected={selectedHosts}
|
40
|
+
setSelected={setSelectedHosts}
|
41
|
+
/>
|
42
|
+
</FormGroup>
|
43
|
+
<span>
|
44
|
+
{__('Apply to')}{' '}
|
45
|
+
<Button variant="link" isInline>
|
46
|
+
{selectedHosts.length} {__('hosts')}
|
47
|
+
</Button>
|
48
|
+
</span>
|
49
|
+
<TemplateInputs
|
50
|
+
inputs={templateInputs}
|
51
|
+
value={templateValues}
|
52
|
+
setValue={setTemplateValues}
|
53
|
+
/>
|
54
|
+
</Form>
|
55
|
+
</>
|
56
|
+
);
|
57
|
+
};
|
58
|
+
|
59
|
+
HostsAndInputs.propTypes = {
|
60
|
+
templateValues: PropTypes.object.isRequired,
|
61
|
+
setTemplateValues: PropTypes.func.isRequired,
|
62
|
+
selectedHosts: PropTypes.array.isRequired,
|
63
|
+
setSelectedHosts: PropTypes.func.isRequired,
|
64
|
+
};
|
65
|
+
|
66
|
+
export default HostsAndInputs;
|
@@ -1,25 +1,28 @@
|
|
1
|
-
import React
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
2
3
|
import { FormGroup, Radio } from '@patternfly/react-core';
|
3
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
4
5
|
|
5
|
-
export const ScheduleType = () =>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
6
|
+
export const ScheduleType = ({ isFuture, setIsFuture }) => (
|
7
|
+
<FormGroup label={__('Schedule type')} fieldId="schedule-type">
|
8
|
+
<Radio
|
9
|
+
isChecked={!isFuture}
|
10
|
+
name="schedule-type"
|
11
|
+
onChange={() => setIsFuture(false)}
|
12
|
+
id="schedule-type-now"
|
13
|
+
label={__('Execute now')}
|
14
|
+
/>
|
15
|
+
<Radio
|
16
|
+
isChecked={isFuture}
|
17
|
+
name="schedule-type"
|
18
|
+
onChange={() => setIsFuture(true)}
|
19
|
+
id="schedule-type-future"
|
20
|
+
label={__('Schedule for future execution')}
|
21
|
+
/>
|
22
|
+
</FormGroup>
|
23
|
+
);
|
24
|
+
|
25
|
+
ScheduleType.propTypes = {
|
26
|
+
isFuture: PropTypes.bool.isRequired,
|
27
|
+
setIsFuture: PropTypes.func.isRequired,
|
25
28
|
};
|
@@ -1,35 +1,48 @@
|
|
1
|
-
import React
|
1
|
+
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
|
-
import { FormGroup,
|
3
|
+
import { FormGroup, Checkbox } from '@patternfly/react-core';
|
4
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
import { DateTimePicker } from '../form/DateTimePicker';
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
export const StartEndDates = ({
|
8
|
+
starts,
|
9
|
+
setStarts,
|
10
|
+
ends,
|
11
|
+
setEnds,
|
12
|
+
isNeverEnds,
|
13
|
+
setIsNeverEnds,
|
14
|
+
}) => {
|
9
15
|
const toggleIsNeverEnds = (checked, event) => {
|
10
16
|
const value = event?.target?.checked;
|
11
17
|
setIsNeverEnds(value);
|
12
|
-
|
18
|
+
};
|
19
|
+
const validateEndDate = () => {
|
20
|
+
if (isNeverEnds) return 'success';
|
21
|
+
if (!starts || !ends) return 'success';
|
22
|
+
if (new Date(starts).getTime() <= new Date(ends).getTime())
|
23
|
+
return 'success';
|
24
|
+
return 'error';
|
13
25
|
};
|
14
26
|
return (
|
15
27
|
<>
|
16
|
-
<FormGroup
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
placeholder="mm/dd/yy, hh:mm UTC"
|
23
|
-
/>
|
28
|
+
<FormGroup
|
29
|
+
className="start-date"
|
30
|
+
label={__('Starts')}
|
31
|
+
fieldId="start-date"
|
32
|
+
>
|
33
|
+
<DateTimePicker dateTime={starts} setDateTime={setStarts} />
|
24
34
|
</FormGroup>
|
25
|
-
<FormGroup
|
26
|
-
|
35
|
+
<FormGroup
|
36
|
+
className="end-date"
|
37
|
+
label={__('Ends')}
|
38
|
+
fieldId="end-date"
|
39
|
+
helperTextInvalid={__('End time needs to be after start time')}
|
40
|
+
validated={validateEndDate()}
|
41
|
+
>
|
42
|
+
<DateTimePicker
|
43
|
+
dateTime={ends}
|
44
|
+
setDateTime={setEnds}
|
27
45
|
isDisabled={isNeverEnds}
|
28
|
-
id="end-date"
|
29
|
-
value={ends}
|
30
|
-
type="text"
|
31
|
-
onChange={newValue => setEnds(newValue)}
|
32
|
-
placeholder="mm/dd/yy, hh:mm UTC"
|
33
46
|
/>
|
34
47
|
<Checkbox
|
35
48
|
label={__('Never ends')}
|
@@ -48,4 +61,6 @@ StartEndDates.propTypes = {
|
|
48
61
|
setStarts: PropTypes.func.isRequired,
|
49
62
|
ends: PropTypes.string.isRequired,
|
50
63
|
setEnds: PropTypes.func.isRequired,
|
64
|
+
isNeverEnds: PropTypes.bool.isRequired,
|
65
|
+
setIsNeverEnds: PropTypes.func.isRequired,
|
51
66
|
};
|