Adds extra variables to schedule details. Updates parameters by which we display prompt fields on schedule details. Extend VariableDetails component to be able to handle values that come in raw JSON form.

This commit is contained in:
mabashian
2020-08-17 16:49:15 -04:00
parent a659b9d994
commit 222a65c875
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);