Merge pull request #7921 from mabashian/6172-schedule-detail-vars

Adds extra variables to schedule details

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-08-24 20:45:32 +00:00 committed by GitHub
commit 5248ac4498
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 43 deletions

View File

@ -1,10 +1,15 @@
import 'styled-components/macro';
import React, { useState, useEffect } from 'react';
import { string, node, number } from 'prop-types';
import { node, number, oneOfType, shape, string } from 'prop-types';
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from '../DetailList';
import MultiButtonToggle from '../MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
import {
yamlToJson,
jsonToYaml,
isJsonObject,
isJsonString,
} from '../../util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
import { JSON_MODE, YAML_MODE } from './constants';
@ -15,7 +20,7 @@ function getValueAsMode(value, mode) {
}
return '---';
}
const modeMatches = isJson(value) === (mode === JSON_MODE);
const modeMatches = isJsonString(value) === (mode === JSON_MODE);
if (modeMatches) {
return value;
}
@ -23,12 +28,21 @@ function getValueAsMode(value, mode) {
}
function VariablesDetail({ value, label, rows, fullHeight }) {
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
const [currentValue, setCurrentValue] = useState(value || '---');
const [mode, setMode] = useState(
isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE
);
const [currentValue, setCurrentValue] = useState(
isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---'
);
const [error, setError] = useState(null);
useEffect(() => {
setCurrentValue(getValueAsMode(value, mode));
setCurrentValue(
getValueAsMode(
isJsonObject(value) ? JSON.stringify(value, null, 2) : value,
mode
)
);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [value]);
@ -95,7 +109,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) {
);
}
VariablesDetail.propTypes = {
value: string.isRequired,
value: oneOfType([shape({}), string]).isRequired,
label: node.isRequired,
rows: number,
};

View File

@ -7,7 +7,7 @@ import styled from 'styled-components';
import { Split, SplitItem } from '@patternfly/react-core';
import { CheckboxField, FieldTooltip } from '../FormField';
import MultiButtonToggle from '../MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
import CodeMirrorInput from './CodeMirrorInput';
import { JSON_MODE, YAML_MODE } from './constants';
@ -30,7 +30,9 @@ function VariablesField({
tooltip,
}) {
const [field, meta, helpers] = useField(name);
const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE);
const [mode, setMode] = useState(
isJsonString(field.value) ? JSON_MODE : YAML_MODE
);
return (
<div className="pf-c-form__group">

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { string, func, bool, number } from 'prop-types';
import { Split, SplitItem } from '@patternfly/react-core';
import styled from 'styled-components';
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
import MultiButtonToggle from '../MultiButtonToggle';
import CodeMirrorInput from './CodeMirrorInput';
import { JSON_MODE, YAML_MODE } from './constants';
@ -18,11 +18,11 @@ const SplitItemRight = styled(SplitItem)`
function VariablesInput(props) {
const { id, label, readOnly, rows, error, onError, className } = props;
/* eslint-disable react/destructuring-assignment */
const defaultValue = isJson(props.value)
const defaultValue = isJsonString(props.value)
? formatJson(props.value)
: props.value;
const [value, setValue] = useState(defaultValue);
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
const [mode, setMode] = useState(isJsonString(value) ? JSON_MODE : YAML_MODE);
const isControlled = !!props.onChange;
/* eslint-enable react/destructuring-assignment */

View File

@ -6,10 +6,12 @@ import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { SchedulesAPI } from '../../api';
import { JobTemplatesAPI, SchedulesAPI } from '../../api';
import Schedule from './Schedule';
jest.mock('../../api/models/JobTemplates');
jest.mock('../../api/models/Schedules');
jest.mock('../../api/models/WorkflowJobTemplates');
SchedulesAPI.readDetail.mockResolvedValue({
data: {
@ -62,6 +64,22 @@ SchedulesAPI.readCredentials.mockResolvedValue({
},
});
JobTemplatesAPI.readLaunch.mockResolvedValue({
data: {
ask_credential_on_launch: false,
ask_diff_mode_on_launch: false,
ask_inventory_on_launch: false,
ask_job_type_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
ask_variables_on_launch: false,
ask_verbosity_on_launch: false,
survey_enabled: false,
},
});
describe('<Schedule />', () => {
let wrapper;
let history;

View File

@ -17,10 +17,15 @@ import ScheduleOccurrences from '../ScheduleOccurrences';
import ScheduleToggle from '../ScheduleToggle';
import { formatDateString } from '../../../util/dates';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { SchedulesAPI } from '../../../api';
import {
JobTemplatesAPI,
SchedulesAPI,
WorkflowJobTemplatesAPI,
} from '../../../api';
import DeleteButton from '../../DeleteButton';
import ErrorDetail from '../../ErrorDetail';
import ChipGroup from '../../ChipGroup';
import { VariablesDetail } from '../../CodeMirrorInput';
const PromptTitle = styled(Title)`
--pf-c-title--m-md--FontWeight: 700;
@ -35,9 +40,9 @@ function ScheduleDetail({ schedule, i18n }) {
diff_mode,
dtend,
dtstart,
extra_data,
job_tags,
job_type,
inventory,
limit,
modified,
name,
@ -67,20 +72,47 @@ function ScheduleDetail({ schedule, i18n }) {
const { error, dismissError } = useDismissableError(deleteError);
const {
result: [credentials, preview],
result: [credentials, preview, launchData],
isLoading,
error: readContentError,
request: fetchCredentialsAndPreview,
} = useRequest(
useCallback(async () => {
const [{ data }, { data: schedulePreview }] = await Promise.all([
const promises = [
SchedulesAPI.readCredentials(id),
SchedulesAPI.createPreview({
rrule,
}),
]);
return [data.results, schedulePreview];
}, [id, rrule]),
];
if (
schedule?.summary_fields?.unified_job_template?.unified_job_type ===
'job'
) {
promises.push(
JobTemplatesAPI.readLaunch(
schedule.summary_fields.unified_job_template.id
)
);
} else if (
schedule?.summary_fields?.unified_job_template?.unified_job_type ===
'workflow_job'
) {
promises.push(
WorkflowJobTemplatesAPI.readLaunch(
schedule.summary_fields.unified_job_template.id
)
);
} else {
promises.push(Promise.resolve());
}
const [{ data }, { data: schedulePreview }, launch] = await Promise.all(
promises
);
return [data.results, schedulePreview, launch?.data];
}, [id, schedule, rrule]),
[]
);
@ -93,15 +125,33 @@ function ScheduleDetail({ schedule, i18n }) {
rule.options.freq === RRule.MINUTELY && dtstart === dtend
? i18n._(t`None (Run Once)`)
: rule.toText().replace(/^\w/, c => c.toUpperCase());
const {
ask_credential_on_launch,
ask_diff_mode_on_launch,
ask_inventory_on_launch,
ask_job_type_on_launch,
ask_limit_on_launch,
ask_scm_branch_on_launch,
ask_skip_tags_on_launch,
ask_tags_on_launch,
ask_variables_on_launch,
ask_verbosity_on_launch,
survey_enabled,
} = launchData || {};
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);
ask_credential_on_launch ||
ask_diff_mode_on_launch ||
ask_inventory_on_launch ||
ask_job_type_on_launch ||
ask_limit_on_launch ||
ask_scm_branch_on_launch ||
ask_skip_tags_on_launch ||
ask_tags_on_launch ||
ask_variables_on_launch ||
ask_verbosity_on_launch ||
survey_enabled;
if (isLoading) {
return <ContentLoading />;
@ -144,8 +194,10 @@ function ScheduleDetail({ schedule, i18n }) {
<PromptTitle headingLevel="h2">
{i18n._(t`Prompted Fields`)}
</PromptTitle>
<Detail label={i18n._(t`Job Type`)} value={job_type} />
{inventory && summary_fields.inventory && (
{ask_job_type_on_launch && (
<Detail label={i18n._(t`Job Type`)} value={job_type} />
)}
{ask_inventory_on_launch && (
<Detail
label={i18n._(t`Inventory`)}
value={
@ -161,18 +213,22 @@ function ScheduleDetail({ schedule, i18n }) {
}
/>
)}
<Detail
label={i18n._(t`Source Control Branch`)}
value={scm_branch}
/>
<Detail label={i18n._(t`Limit`)} value={limit} />
{typeof diff_mode === 'boolean' && (
{ask_scm_branch_on_launch && (
<Detail
label={i18n._(t`Source Control Branch`)}
value={scm_branch}
/>
)}
{ask_limit_on_launch && (
<Detail label={i18n._(t`Limit`)} value={limit} />
)}
{ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && (
<Detail
label={i18n._(t`Show Changes`)}
value={diff_mode ? 'On' : 'Off'}
/>
)}
{credentials && credentials.length > 0 && (
{ask_credential_on_launch && (
<Detail
fullWidth
label={i18n._(t`Credentials`)}
@ -185,7 +241,7 @@ function ScheduleDetail({ schedule, i18n }) {
}
/>
)}
{job_tags && job_tags.length > 0 && (
{ask_tags_on_launch && job_tags && job_tags.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Job Tags`)}
@ -203,7 +259,7 @@ function ScheduleDetail({ schedule, i18n }) {
}
/>
)}
{skip_tags && skip_tags.length > 0 && (
{ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && (
<Detail
fullWidth
label={i18n._(t`Skip Tags`)}
@ -221,6 +277,13 @@ function ScheduleDetail({ schedule, i18n }) {
}
/>
)}
{(ask_variables_on_launch || survey_enabled) && (
<VariablesDetail
value={extra_data}
rows={4}
label={i18n._(t`Variables`)}
/>
)}
</>
)}
</DetailList>

View File

@ -2,14 +2,48 @@ import React from 'react';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { SchedulesAPI } from '../../../api';
import { SchedulesAPI, JobTemplatesAPI } from '../../../api';
import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import ScheduleDetail from './ScheduleDetail';
jest.mock('../../../api/models/JobTemplates');
jest.mock('../../../api/models/Schedules');
jest.mock('../../../api/models/WorkflowJobTemplates');
const allPrompts = {
data: {
ask_credential_on_launch: true,
ask_diff_mode_on_launch: true,
ask_inventory_on_launch: true,
ask_job_type_on_launch: true,
ask_limit_on_launch: true,
ask_scm_branch_on_launch: true,
ask_skip_tags_on_launch: true,
ask_tags_on_launch: true,
ask_variables_on_launch: true,
ask_verbosity_on_launch: true,
survey_enabled: true,
},
};
const noPrompts = {
data: {
ask_credential_on_launch: false,
ask_diff_mode_on_launch: false,
ask_inventory_on_launch: false,
ask_job_type_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
ask_variables_on_launch: false,
ask_verbosity_on_launch: false,
survey_enabled: false,
},
};
const schedule = {
url: '/api/v2/schedules/1',
@ -53,6 +87,7 @@ const schedule = {
dtstart: '2020-03-16T04:00:00Z',
dtend: '2020-07-06T04:00:00Z',
next_run: '2020-03-16T04:00:00Z',
extra_data: {},
};
SchedulesAPI.createPreview.mockResolvedValue({
@ -79,6 +114,7 @@ describe('<ScheduleDetail />', () => {
results: [],
},
});
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
<Route
@ -134,6 +170,7 @@ describe('<ScheduleDetail />', () => {
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);
expect(wrapper.find('VariablesDetail').length).toBe(0);
});
test('details should render with the proper values with prompts', async () => {
SchedulesAPI.readCredentials.mockResolvedValue({
@ -151,6 +188,7 @@ describe('<ScheduleDetail />', () => {
],
},
});
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts);
const scheduleWithPrompts = {
...schedule,
job_type: 'run',
@ -161,6 +199,7 @@ describe('<ScheduleDetail />', () => {
limit: 'localhost',
diff_mode: true,
verbosity: 1,
extra_data: { foo: 'fii' },
};
await act(async () => {
wrapper = mountWithContexts(
@ -182,7 +221,6 @@ describe('<ScheduleDetail />', () => {
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
// await waitForElement(wrapper, 'Title', el => el.length > 0);
expect(
wrapper
.find('Detail[label="Name"]')
@ -231,6 +269,7 @@ describe('<ScheduleDetail />', () => {
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);
expect(wrapper.find('VariablesDetail').length).toBe(1);
});
test('error shown when error encountered fetching credentials', async () => {
SchedulesAPI.readCredentials.mockRejectedValueOnce(
@ -245,6 +284,7 @@ describe('<ScheduleDetail />', () => {
},
})
);
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
<Route
@ -274,6 +314,7 @@ describe('<ScheduleDetail />', () => {
results: [],
},
});
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
<Route
@ -313,6 +354,7 @@ describe('<ScheduleDetail />', () => {
results: [],
},
});
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
<Route

View File

@ -22,7 +22,11 @@ export function jsonToYaml(jsonString) {
return yaml.safeDump(value);
}
export function isJson(jsonString) {
export function isJsonObject(value) {
return typeof value === 'object' && value !== null;
}
export function isJsonString(jsonString) {
if (typeof jsonString !== 'string') {
return false;
}
@ -40,7 +44,7 @@ export function parseVariableField(variableField) {
if (variableField === '---' || variableField === '{}') {
return {};
}
if (!isJson(variableField)) {
if (!isJsonString(variableField)) {
variableField = yamlToJson(variableField);
}
variableField = JSON.parse(variableField);