mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 13:55:31 +03:00
Merge pull request #6170 from mabashian/5859-jt-schedule-details
Adds generic schedule detail component and applies it to JT/WFJT/Proj schedules Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
f4366be419
22
awx/ui_next/package-lock.json
generated
22
awx/ui_next/package-lock.json
generated
@ -13202,6 +13202,12 @@
|
||||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"luxon": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.22.0.tgz",
|
||||
"integrity": "sha512-3sLvlfbFo+AxVEY3IqxymbumtnlgBwjDExxK60W3d+trrUzErNAz/PfvPT+mva+vEUrdIodeCOs7fB6zHtRSrw==",
|
||||
"optional": true
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
@ -16217,6 +16223,22 @@
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"rrule": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz",
|
||||
"integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==",
|
||||
"requires": {
|
||||
"luxon": "^1.21.3",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
|
||||
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"rst-selector-parser": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
|
||||
|
@ -77,6 +77,7 @@
|
||||
"react-dom": "^16.13.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-virtualized": "^9.21.1",
|
||||
"rrule": "^2.6.4",
|
||||
"styled-components": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,14 @@ class Schedules extends Base {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/schedules/';
|
||||
}
|
||||
|
||||
createPreview(data) {
|
||||
return this.http.post(`${this.baseUrl}preview/`, data);
|
||||
}
|
||||
|
||||
readCredentials(resourceId, params) {
|
||||
return this.http.get(`${this.baseUrl}${resourceId}/credentials/`, params);
|
||||
}
|
||||
}
|
||||
|
||||
export default Schedules;
|
||||
|
@ -21,6 +21,7 @@ function MultiButtonToggle({ buttons, value, onChange }) {
|
||||
{buttons &&
|
||||
buttons.map(([buttonValue, buttonLabel]) => (
|
||||
<SmallButton
|
||||
aria-label={buttonLabel}
|
||||
key={buttonLabel}
|
||||
onClick={() => setValue(buttonValue)}
|
||||
variant={buttonValue === value ? 'primary' : 'secondary'}
|
||||
|
140
awx/ui_next/src/components/Schedule/Schedule.jsx
Normal file
140
awx/ui_next/src/components/Schedule/Schedule.jsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import {
|
||||
Switch,
|
||||
Route,
|
||||
Link,
|
||||
Redirect,
|
||||
useLocation,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { CardActions } from '@patternfly/react-core';
|
||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import { ScheduleDetail } from '@components/Schedule';
|
||||
import { SchedulesAPI } from '@api';
|
||||
|
||||
function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
||||
const [schedule, setSchedule] = useState(null);
|
||||
const [contentLoading, setContentLoading] = useState(true);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const { scheduleId } = useParams();
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const { data } = await SchedulesAPI.readDetail(scheduleId);
|
||||
setSchedule(data);
|
||||
setBreadcrumb(unifiedJobTemplate, data);
|
||||
} catch (err) {
|
||||
setContentError(err);
|
||||
} finally {
|
||||
setContentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [location.pathname, scheduleId, unifiedJobTemplate, setBreadcrumb]);
|
||||
|
||||
const tabsArray = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<CaretLeftIcon />
|
||||
{i18n._(t`Back to Schedules`)}
|
||||
</>
|
||||
),
|
||||
link: `${pathRoot}schedules`,
|
||||
id: 99,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
link: `${pathRoot}schedules/${schedule && schedule.id}/details`,
|
||||
id: 0,
|
||||
},
|
||||
];
|
||||
|
||||
if (contentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (
|
||||
schedule.summary_fields.unified_job_template.id !==
|
||||
parseInt(unifiedJobTemplate.id, 10)
|
||||
) {
|
||||
return (
|
||||
<ContentError>
|
||||
{schedule && (
|
||||
<Link to={`${pathRoot}schedules`}>{i18n._(t`View Schedules`)}</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
);
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
let cardHeader = null;
|
||||
if (
|
||||
location.pathname.includes('schedules/') &&
|
||||
!location.pathname.endsWith('edit')
|
||||
) {
|
||||
cardHeader = (
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<CardActions>
|
||||
<CardCloseButton linkTo={`${pathRoot}schedules`} />
|
||||
</CardActions>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{cardHeader}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from={`${pathRoot}schedules/:scheduleId`}
|
||||
to={`${pathRoot}schedules/:scheduleId/details`}
|
||||
exact
|
||||
/>
|
||||
{schedule && [
|
||||
<Route
|
||||
key="details"
|
||||
path={`${pathRoot}schedules/:scheduleId/details`}
|
||||
render={() => {
|
||||
return <ScheduleDetail schedule={schedule} />;
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
<Route
|
||||
key="not-found"
|
||||
path="*"
|
||||
render={() => {
|
||||
return (
|
||||
<ContentError>
|
||||
{unifiedJobTemplate && (
|
||||
<Link to={`${pathRoot}details`}>
|
||||
{i18n._(t`View Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Schedule as _Schedule };
|
||||
export default withI18n()(Schedule);
|
110
awx/ui_next/src/components/Schedule/Schedule.test.jsx
Normal file
110
awx/ui_next/src/components/Schedule/Schedule.test.jsx
Normal file
@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import Schedule from './Schedule';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
||||
SchedulesAPI.readDetail.mockResolvedValue({
|
||||
data: {
|
||||
url: '/api/v2/schedules/1',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1',
|
||||
id: 1,
|
||||
summary_fields: {
|
||||
unified_job_template: {
|
||||
id: 1,
|
||||
name: 'Mock JT',
|
||||
description: '',
|
||||
unified_job_type: 'job',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
modified_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
},
|
||||
created: '2020-03-03T20:38:54.210306Z',
|
||||
modified: '2020-03-03T20:38:54.210336Z',
|
||||
name: 'Mock JT Schedule',
|
||||
next_run: '2020-02-20T05:00:00Z',
|
||||
},
|
||||
});
|
||||
|
||||
SchedulesAPI.createPreview.mockResolvedValue({
|
||||
data: {
|
||||
local: [],
|
||||
utc: [],
|
||||
},
|
||||
});
|
||||
|
||||
SchedulesAPI.readCredentials.mockResolvedValue({
|
||||
data: {
|
||||
count: 0,
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('<Schedule />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
const unifiedJobTemplate = { id: 1, name: 'Mock JT' };
|
||||
beforeAll(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/templates/job_template/1/schedules/1/details'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/templates/job_template/:id/schedules"
|
||||
component={() => (
|
||||
<Schedule
|
||||
setBreadcrumb={() => {}}
|
||||
unifiedJobTemplate={unifiedJobTemplate}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: {
|
||||
params: { id: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('renders successfully', async () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('expect all tabs to exist, including Back to Schedules', async () => {
|
||||
expect(
|
||||
wrapper.find('button[link="/templates/job_template/1/schedules"]').length
|
||||
).toBe(1);
|
||||
expect(wrapper.find('button[aria-label="Details"]').length).toBe(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,202 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { rrulestr } from 'rrule';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Schedule } from '@types';
|
||||
import { Chip, ChipGroup, Title } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import CredentialChip from '@components/CredentialChip';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
import { ScheduleOccurrences, ScheduleToggle } from '@components/Schedule';
|
||||
import { formatDateString } from '@util/dates';
|
||||
import useRequest from '@util/useRequest';
|
||||
import { SchedulesAPI } from '@api';
|
||||
|
||||
const PromptTitle = styled(Title)`
|
||||
--pf-c-title--m-md--FontWeight: 700;
|
||||
`;
|
||||
|
||||
function ScheduleDetail({ schedule, i18n }) {
|
||||
const {
|
||||
id,
|
||||
created,
|
||||
description,
|
||||
diff_mode,
|
||||
dtend,
|
||||
dtstart,
|
||||
job_tags,
|
||||
job_type,
|
||||
inventory,
|
||||
limit,
|
||||
modified,
|
||||
name,
|
||||
next_run,
|
||||
rrule,
|
||||
scm_branch,
|
||||
skip_tags,
|
||||
summary_fields,
|
||||
timezone,
|
||||
} = schedule;
|
||||
|
||||
const {
|
||||
result: [credentials, preview],
|
||||
isLoading,
|
||||
error,
|
||||
request: fetchCredentialsAndPreview,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [{ data }, { data: schedulePreview }] = await Promise.all([
|
||||
SchedulesAPI.readCredentials(id),
|
||||
SchedulesAPI.createPreview({
|
||||
rrule,
|
||||
}),
|
||||
]);
|
||||
return [data.results, schedulePreview];
|
||||
}, [id, rrule]),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredentialsAndPreview();
|
||||
}, [fetchCredentialsAndPreview]);
|
||||
|
||||
const rule = rrulestr(rrule);
|
||||
const repeatFrequency =
|
||||
rule.options.freq === 3 && dtstart === dtend
|
||||
? i18n._(t`None (Run Once)`)
|
||||
: rule.toText().replace(/^\w/, c => c.toUpperCase());
|
||||
const showPromptedFields =
|
||||
(credentials && credentials.length > 0) ||
|
||||
job_type ||
|
||||
(inventory && summary_fields.inventory) ||
|
||||
scm_branch ||
|
||||
limit ||
|
||||
typeof diff_mode === 'boolean' ||
|
||||
(job_tags && job_tags.length > 0) ||
|
||||
(skip_tags && skip_tags.length > 0);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<ScheduleToggle schedule={schedule} css="padding-bottom: 40px" />
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<Detail
|
||||
label={i18n._(t`First Run`)}
|
||||
value={formatDateString(dtstart)}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Next Run`)}
|
||||
value={formatDateString(next_run)}
|
||||
/>
|
||||
<Detail label={i18n._(t`Last Run`)} value={formatDateString(dtend)} />
|
||||
<Detail label={i18n._(t`Local Time Zone`)} value={timezone} />
|
||||
<Detail label={i18n._(t`Repeat Frequency`)} value={repeatFrequency} />
|
||||
<ScheduleOccurrences preview={preview} />
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={summary_fields.created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
date={modified}
|
||||
user={summary_fields.modified_by}
|
||||
/>
|
||||
{showPromptedFields && (
|
||||
<>
|
||||
<PromptTitle size="md" css="grid-column: 1 / -1;">
|
||||
{i18n._(t`Prompted Fields`)}
|
||||
</PromptTitle>
|
||||
<Detail label={i18n._(t`Job Type`)} value={job_type} />
|
||||
{inventory && summary_fields.inventory && (
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
value={
|
||||
<Link
|
||||
to={`/inventories/${
|
||||
summary_fields.inventory.kind === 'smart'
|
||||
? 'smart_inventory'
|
||||
: 'inventory'
|
||||
}/${summary_fields.inventory.id}/details`}
|
||||
>
|
||||
{summary_fields.inventory.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} />
|
||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
||||
{typeof diff_mode === 'boolean' && (
|
||||
<Detail
|
||||
label={i18n._(t`Show Changes`)}
|
||||
value={diff_mode ? 'On' : 'Off'}
|
||||
/>
|
||||
)}
|
||||
{credentials && credentials.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Credentials`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{credentials.map(c => (
|
||||
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{job_tags && job_tags.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Job Tags`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{job_tags.split(',').map(jobTag => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{skip_tags && skip_tags.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Skip Tags`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{skip_tags.split(',').map(skipTag => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DetailList>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleDetail.propTypes = {
|
||||
schedule: Schedule.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(ScheduleDetail);
|
@ -0,0 +1,261 @@
|
||||
import React from 'react';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import ScheduleDetail from './ScheduleDetail';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
||||
const schedule = {
|
||||
url: '/api/v2/schedules/1',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1',
|
||||
id: 1,
|
||||
summary_fields: {
|
||||
unified_job_template: {
|
||||
id: 1,
|
||||
name: 'Mock JT',
|
||||
description: '',
|
||||
unified_job_type: 'job',
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
modified_by: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
inventory: {
|
||||
id: 1,
|
||||
name: 'Test Inventory',
|
||||
},
|
||||
},
|
||||
created: '2020-03-03T20:38:54.210306Z',
|
||||
modified: '2020-03-03T20:38:54.210336Z',
|
||||
name: 'Mock JT Schedule',
|
||||
enabled: false,
|
||||
description: 'A good schedule',
|
||||
timezone: 'America/New_York',
|
||||
dtstart: '2020-03-16T04:00:00Z',
|
||||
dtend: '2020-07-06T04:00:00Z',
|
||||
next_run: '2020-03-16T04:00:00Z',
|
||||
};
|
||||
|
||||
SchedulesAPI.createPreview.mockResolvedValue({
|
||||
data: {
|
||||
local: [],
|
||||
utc: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('<ScheduleDetail />', () => {
|
||||
let wrapper;
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates/job_template/1/schedules/1/details'],
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('details should render with the proper values without prompts', async () => {
|
||||
SchedulesAPI.readCredentials.mockResolvedValueOnce({
|
||||
data: {
|
||||
count: 0,
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/templates/job_template/:id/schedules/:scheduleId"
|
||||
component={() => <ScheduleDetail schedule={schedule} />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 1 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Name"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('Mock JT Schedule');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Description"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('A good schedule');
|
||||
expect(wrapper.find('Detail[label="First Run"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Next Run"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Last Run"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Local Time Zone"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('America/New_York');
|
||||
expect(wrapper.find('Detail[label="Repeat Frequency"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Created"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1);
|
||||
expect(wrapper.find('Title[children="Prompted Fields"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Job Type"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Inventory"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="SCM Branch"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Limit"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Show Changes"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Credentials"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0);
|
||||
});
|
||||
test('details should render with the proper values with prompts', async () => {
|
||||
SchedulesAPI.readCredentials.mockResolvedValueOnce({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Cred 1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Cred 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const scheduleWithPrompts = {
|
||||
...schedule,
|
||||
job_type: 'run',
|
||||
inventory: 1,
|
||||
job_tags: 'tag1',
|
||||
skip_tags: 'tag2',
|
||||
scm_branch: 'foo/branch',
|
||||
limit: 'localhost',
|
||||
diff_mode: true,
|
||||
verbosity: 1,
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/templates/job_template/:id/schedules/:scheduleId"
|
||||
component={() => <ScheduleDetail schedule={scheduleWithPrompts} />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 1 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Name"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('Mock JT Schedule');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Description"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('A good schedule');
|
||||
expect(wrapper.find('Detail[label="First Run"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Next Run"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Last Run"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Local Time Zone"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('America/New_York');
|
||||
expect(wrapper.find('Detail[label="Repeat Frequency"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Created"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1);
|
||||
expect(wrapper.find('Title[children="Prompted Fields"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Job Type"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('run');
|
||||
expect(wrapper.find('Detail[label="Inventory"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="SCM Branch"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('foo/branch');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Limit"]')
|
||||
.find('dd')
|
||||
.text()
|
||||
).toBe('localhost');
|
||||
expect(wrapper.find('Detail[label="Show Changes"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Credentials"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(1);
|
||||
});
|
||||
test('error shown when error encountered fetching credentials', async () => {
|
||||
SchedulesAPI.readCredentials.mockRejectedValueOnce(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/job_templates/1/schedules/1/credentials',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 500,
|
||||
},
|
||||
})
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/templates/job_template/:id/schedules/:scheduleId"
|
||||
component={() => <ScheduleDetail schedule={schedule} />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 1 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
export { default } from './ScheduleDetail';
|
@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import ScheduleList from './ScheduleList';
|
||||
import mockSchedules from './data.schedules.json';
|
||||
import mockSchedules from '../data.schedules.json';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
@ -15,10 +15,10 @@ import {
|
||||
} from '@patternfly/react-core';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { ScheduleToggle } from '@components/Schedule';
|
||||
import styled from 'styled-components';
|
||||
import { Schedule } from '@types';
|
||||
import { formatDateString } from '@util/dates';
|
||||
import ScheduleToggle from './ScheduleToggle';
|
||||
|
||||
const DataListAction = styled(_DataListAction)`
|
||||
align-items: center;
|
@ -0,0 +1,79 @@
|
||||
import React, { useState } from 'react';
|
||||
import { shape } from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { formatDateString, formatDateStringUTC } from '@util/dates';
|
||||
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
||||
import { DetailName, DetailValue } from '@components/DetailList';
|
||||
import MultiButtonToggle from '@components/MultiButtonToggle';
|
||||
|
||||
const OccurrencesLabel = styled.div`
|
||||
display: inline-block;
|
||||
font-size: var(--pf-c-form__label--FontSize);
|
||||
font-weight: var(--pf-c-form__label--FontWeight);
|
||||
line-height: var(--pf-c-form__label--LineHeight);
|
||||
color: var(--pf-c-form__label--Color);
|
||||
|
||||
span:first-of-type {
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
function ScheduleOccurrences({ preview = { local: [], utc: [] }, i18n }) {
|
||||
const [mode, setMode] = useState('local');
|
||||
|
||||
if (preview.local.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailName
|
||||
component={TextListItemVariants.dt}
|
||||
fullWidth
|
||||
css="grid-column: 1 / -1"
|
||||
>
|
||||
<Split gutter="sm">
|
||||
<SplitItem>
|
||||
<OccurrencesLabel>
|
||||
<span>{i18n._(t`Occurrences`)}</span>
|
||||
<span>{i18n._(t`(Limited to first 10)`)}</span>
|
||||
</OccurrencesLabel>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<MultiButtonToggle
|
||||
buttons={[['local', 'Local'], ['utc', 'UTC']]}
|
||||
value={mode}
|
||||
onChange={newMode => setMode(newMode)}
|
||||
/>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</DetailName>
|
||||
<DetailValue
|
||||
component={TextListItemVariants.dd}
|
||||
fullWidth
|
||||
css="grid-column: 1 / -1; margin-top: -10px"
|
||||
>
|
||||
{preview[mode].map(dateStr => (
|
||||
<div key={dateStr}>
|
||||
{mode === 'local'
|
||||
? formatDateString(dateStr)
|
||||
: formatDateStringUTC(dateStr)}
|
||||
</div>
|
||||
))}
|
||||
</DetailValue>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleOccurrences.propTypes = {
|
||||
preview: shape(),
|
||||
};
|
||||
|
||||
ScheduleOccurrences.defaultProps = {
|
||||
preview: { local: [], utc: [] },
|
||||
};
|
||||
|
||||
export default withI18n()(ScheduleOccurrences);
|
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import ScheduleOccurrences from './ScheduleOccurrences';
|
||||
|
||||
describe('<ScheduleOccurrences>', () => {
|
||||
let wrapper;
|
||||
describe('At least two dates passed in', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleOccurrences
|
||||
preview={{
|
||||
local: ['2020-03-16T00:00:00-04:00', '2020-03-30T00:00:00-04:00'],
|
||||
utc: ['2020-03-16T04:00:00Z', '2020-03-30T04:00:00Z'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('Local option initially set', async () => {
|
||||
expect(wrapper.find('MultiButtonToggle').props().value).toBe('local');
|
||||
});
|
||||
test('It renders the correct number of dates', async () => {
|
||||
expect(wrapper.find('dd').children().length).toBe(2);
|
||||
});
|
||||
test('Clicking UTC button toggles the dates to utc', async () => {
|
||||
wrapper.find('button[aria-label="UTC"]').simulate('click');
|
||||
expect(wrapper.find('MultiButtonToggle').props().value).toBe('utc');
|
||||
expect(wrapper.find('dd').children().length).toBe(2);
|
||||
expect(
|
||||
wrapper
|
||||
.find('dd')
|
||||
.children()
|
||||
.at(0)
|
||||
.text()
|
||||
).toBe('3/16/2020, 4:00:00 AM');
|
||||
expect(
|
||||
wrapper
|
||||
.find('dd')
|
||||
.children()
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe('3/30/2020, 4:00:00 AM');
|
||||
});
|
||||
});
|
||||
describe('Only one date passed in', () => {
|
||||
test('Component should not render chldren', async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleOccurrences
|
||||
preview={{
|
||||
local: ['2020-03-16T00:00:00-04:00'],
|
||||
utc: ['2020-03-16T04:00:00Z'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleOccurrences').children().length).toBe(0);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
export { default } from './ScheduleOccurrences';
|
@ -0,0 +1 @@
|
||||
export { default } from './ScheduleToggle';
|
43
awx/ui_next/src/components/Schedule/Schedules.jsx
Normal file
43
awx/ui_next/src/components/Schedule/Schedules.jsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
import { Schedule, ScheduleList } from '@components/Schedule';
|
||||
|
||||
function Schedules({
|
||||
setBreadcrumb,
|
||||
unifiedJobTemplate,
|
||||
loadSchedules,
|
||||
loadScheduleOptions,
|
||||
}) {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
key="details"
|
||||
path={`${match.path}/:scheduleId`}
|
||||
render={() => (
|
||||
<Schedule
|
||||
unifiedJobTemplate={unifiedJobTemplate}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
key="list"
|
||||
path={`${match.path}`}
|
||||
render={() => {
|
||||
return (
|
||||
<ScheduleList
|
||||
loadSchedules={loadSchedules}
|
||||
loadScheduleOptions={loadScheduleOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export { Schedules as _Schedules };
|
||||
export default withI18n()(Schedules);
|
33
awx/ui_next/src/components/Schedule/Schedules.test.jsx
Normal file
33
awx/ui_next/src/components/Schedule/Schedules.test.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import Schedules from './Schedules';
|
||||
|
||||
describe('<Schedules />', () => {
|
||||
test('initially renders successfully', async () => {
|
||||
let wrapper;
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/templates/job_template/1/schedules'],
|
||||
});
|
||||
const jobTemplate = { id: 1, name: 'Mock JT' };
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Schedules
|
||||
setBreadcrumb={() => {}}
|
||||
jobTemplate={jobTemplate}
|
||||
loadSchedules={() => {}}
|
||||
loadScheduleOptions={() => {}}
|
||||
/>,
|
||||
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
});
|
6
awx/ui_next/src/components/Schedule/index.js
Normal file
6
awx/ui_next/src/components/Schedule/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as Schedule } from './Schedule';
|
||||
export { default as Schedules } from './Schedules';
|
||||
export { default as ScheduleList } from './ScheduleList';
|
||||
export { default as ScheduleOccurrences } from './ScheduleOccurrences';
|
||||
export { default as ScheduleToggle } from './ScheduleToggle';
|
||||
export { default as ScheduleDetail } from './ScheduleDetail';
|
@ -9,7 +9,7 @@ import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import NotificationList from '@components/NotificationList';
|
||||
import { ResourceAccessList } from '@components/ResourceAccessList';
|
||||
import ScheduleList from '@components/ScheduleList';
|
||||
import { Schedules } from '@components/Schedule';
|
||||
import ProjectDetail from './ProjectDetail';
|
||||
import ProjectEdit from './ProjectEdit';
|
||||
import ProjectJobTemplatesList from './ProjectJobTemplatesList';
|
||||
@ -116,7 +116,7 @@ class Project extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { location, match, me, i18n } = this.props;
|
||||
const { location, match, me, i18n, setBreadcrumb } = this.props;
|
||||
|
||||
const {
|
||||
project,
|
||||
@ -175,7 +175,10 @@ class Project extends Component {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
if (
|
||||
location.pathname.endsWith('edit') ||
|
||||
location.pathname.includes('schedules/')
|
||||
) {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
@ -247,7 +250,9 @@ class Project extends Component {
|
||||
<Route
|
||||
path="/projects/:id/schedules"
|
||||
render={() => (
|
||||
<ScheduleList
|
||||
<Schedules
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
unifiedJobTemplate={project}
|
||||
loadSchedules={this.loadSchedules}
|
||||
loadScheduleOptions={this.loadScheduleOptions}
|
||||
/>
|
||||
|
@ -4,11 +4,11 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import Breadcrumbs from '@components/Breadcrumbs';
|
||||
import ScheduleList from '@components/ScheduleList';
|
||||
import { ScheduleList } from '@components/Schedule';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
|
||||
function Schedules({ i18n }) {
|
||||
function AllSchedules({ i18n }) {
|
||||
const loadScheduleOptions = () => {
|
||||
return SchedulesAPI.readOptions();
|
||||
};
|
||||
@ -41,4 +41,4 @@ function Schedules({ i18n }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(Schedules);
|
||||
export default withI18n()(AllSchedules);
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import Schedules from './Schedules';
|
||||
import AllSchedules from './AllSchedules';
|
||||
|
||||
describe('<Schedules />', () => {
|
||||
describe('<AllSchedules />', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
@ -11,7 +11,7 @@ describe('<Schedules />', () => {
|
||||
});
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
wrapper = mountWithContexts(<Schedules />);
|
||||
wrapper = mountWithContexts(<AllSchedules />);
|
||||
});
|
||||
|
||||
test('should display schedule list breadcrumb heading', () => {
|
||||
@ -19,7 +19,7 @@ describe('<Schedules />', () => {
|
||||
initialEntries: ['/schedules'],
|
||||
});
|
||||
|
||||
wrapper = mountWithContexts(<Schedules />, {
|
||||
wrapper = mountWithContexts(<AllSchedules />, {
|
||||
context: {
|
||||
router: {
|
||||
history,
|
@ -1 +1 @@
|
||||
export { default } from './Schedules';
|
||||
export { default } from './AllSchedules';
|
||||
|
@ -10,7 +10,7 @@ import ContentError from '@components/ContentError';
|
||||
import JobList from '@components/JobList';
|
||||
import NotificationList from '@components/NotificationList';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ScheduleList from '@components/ScheduleList';
|
||||
import { Schedules } from '@components/Schedule';
|
||||
import { ResourceAccessList } from '@components/ResourceAccessList';
|
||||
import JobTemplateDetail from './JobTemplateDetail';
|
||||
import JobTemplateEdit from './JobTemplateEdit';
|
||||
@ -96,7 +96,7 @@ class Template extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { i18n, location, match, me } = this.props;
|
||||
const { i18n, location, match, me, setBreadcrumb } = this.props;
|
||||
const {
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
@ -126,6 +126,10 @@ class Template extends Component {
|
||||
}
|
||||
|
||||
tabsArray.push(
|
||||
{
|
||||
name: i18n._(t`Schedules`),
|
||||
link: `${match.url}/schedules`,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Completed Jobs`),
|
||||
link: `${match.url}/completed_jobs`,
|
||||
@ -149,7 +153,10 @@ class Template extends Component {
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
if (
|
||||
location.pathname.endsWith('edit') ||
|
||||
location.pathname.includes('schedules/')
|
||||
) {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
@ -211,6 +218,20 @@ class Template extends Component {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
key="schedules"
|
||||
path="/templates/:templateType/:id/schedules"
|
||||
render={() => (
|
||||
<Schedules
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
unifiedJobTemplate={template}
|
||||
loadSchedules={this.loadSchedules}
|
||||
loadScheduleOptions={this.loadScheduleOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{canSeeNotificationsTab && (
|
||||
<Route
|
||||
path="/templates/:templateType/:id/notifications"
|
||||
@ -228,17 +249,6 @@ class Template extends Component {
|
||||
<JobList defaultParams={{ job__job_template: template.id }} />
|
||||
</Route>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
path="/templates/:templateType/:id/schedules"
|
||||
render={() => (
|
||||
<ScheduleList
|
||||
loadSchedules={this.loadSchedules}
|
||||
loadScheduleOptions={this.loadScheduleOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
path="/templates/:templateType/:id/survey"
|
||||
|
@ -58,10 +58,11 @@ describe('<Template />', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<Template setBreadcrumb={() => {}} me={mockMe} />
|
||||
);
|
||||
|
||||
const tabs = await waitForElement(
|
||||
wrapper,
|
||||
'.pf-c-tabs__item',
|
||||
el => el.length === 6
|
||||
el => el.length === 7
|
||||
);
|
||||
expect(tabs.at(2).text()).toEqual('Notifications');
|
||||
done();
|
||||
|
@ -27,7 +27,7 @@ class Templates extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
setBreadCrumbConfig = template => {
|
||||
setBreadCrumbConfig = (template, schedule) => {
|
||||
const { i18n } = this.props;
|
||||
if (!template) {
|
||||
return;
|
||||
@ -53,6 +53,13 @@ class Templates extends Component {
|
||||
t`Completed Jobs`
|
||||
),
|
||||
[`/templates/${template.type}/${template.id}/survey`]: i18n._(t`Survey`),
|
||||
[`/templates/${template.type}/${template.id}/schedules`]: i18n._(
|
||||
t`Schedules`
|
||||
),
|
||||
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
|
||||
schedule.id}`]: `${schedule && schedule.name}`,
|
||||
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
|
||||
schedule.id}/details`]: i18n._(t`Schedule Details`),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ import ContentError from '@components/ContentError';
|
||||
import FullPage from '@components/FullPage';
|
||||
import JobList from '@components/JobList';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ScheduleList from '@components/ScheduleList';
|
||||
import { Schedules } from '@components/Schedule';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
|
||||
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
||||
@ -88,7 +88,7 @@ class WorkflowJobTemplate extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { i18n, location, match } = this.props;
|
||||
const { i18n, location, match, setBreadcrumb } = this.props;
|
||||
const {
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
@ -152,7 +152,10 @@ class WorkflowJobTemplate extends Component {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
{location.pathname.endsWith('edit') ? null : cardHeader}
|
||||
{location.pathname.endsWith('edit') ||
|
||||
location.pathname.includes('schedules/')
|
||||
? null
|
||||
: cardHeader}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/templates/workflow_job_template/:id"
|
||||
@ -207,9 +210,11 @@ class WorkflowJobTemplate extends Component {
|
||||
)}
|
||||
{template?.id && (
|
||||
<Route
|
||||
path="/templates/:templateType/:id/schedules"
|
||||
path="/templates/workflow_job_template/:id/schedules"
|
||||
render={() => (
|
||||
<ScheduleList
|
||||
<Schedules
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
unifiedJobTemplate={template}
|
||||
loadSchedules={this.loadSchedules}
|
||||
loadScheduleOptions={this.loadScheduleOptions}
|
||||
/>
|
||||
|
@ -291,7 +291,7 @@ export const Schedule = shape({
|
||||
skip_tags: string,
|
||||
limit: string,
|
||||
diff_mode: bool,
|
||||
verbosity: string,
|
||||
verbosity: number,
|
||||
unified_job_template: number,
|
||||
enabled: bool,
|
||||
dtstart: string,
|
||||
|
@ -5,6 +5,10 @@ export function formatDateString(dateString, lang = getLanguage(navigator)) {
|
||||
return new Date(dateString).toLocaleString(lang);
|
||||
}
|
||||
|
||||
export function formatDateStringUTC(dateString, lang = getLanguage(navigator)) {
|
||||
return new Date(dateString).toLocaleString(lang, { timeZone: 'UTC' });
|
||||
}
|
||||
|
||||
export function secondsToHHMMSS(seconds) {
|
||||
return new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user