Partition base resource into defaults and overrides

This commit is contained in:
Marliana Lara
2020-04-13 11:33:39 -04:00
parent 98e8a09ad3
commit c6111fface
4 changed files with 233 additions and 88 deletions

View File

@@ -4,6 +4,7 @@ import { withI18n } from '@lingui/react';
import { t, Trans } from '@lingui/macro'; import { t, Trans } from '@lingui/macro';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import { toTitleCase } from '@util/strings';
import { Chip, ChipGroup } from '@patternfly/react-core'; import { Chip, ChipGroup } from '@patternfly/react-core';
import { VariablesDetail } from '@components/CodeMirrorInput'; import { VariablesDetail } from '@components/CodeMirrorInput';
@@ -19,6 +20,19 @@ const PromptHeader = styled.h2`
margin: var(--pf-global--spacer--lg) 0; margin: var(--pf-global--spacer--lg) 0;
`; `;
function formatTimeout(timeout) {
if (typeof timeout === 'undefined' || timeout === null) {
return null;
}
const minutes = Math.floor(timeout / 60);
const seconds = timeout - Math.floor(timeout / 60) * 60;
return (
<>
{minutes} <Trans>min</Trans> {seconds} <Trans>sec</Trans>
</>
);
}
function hasPromptData(launchData) { function hasPromptData(launchData) {
return ( return (
launchData.ask_credential_on_launch || launchData.ask_credential_on_launch ||
@@ -34,21 +48,107 @@ function hasPromptData(launchData) {
); );
} }
function formatTimeout(timeout) { function removeOverrides(resource, overrides) {
if (typeof timeout === 'undefined' || timeout === null) { const filteredResource = Object.keys(overrides).reduce(
return null; (acc, value) => {
} let root;
const minutes = Math.floor(timeout / 60); let nested;
const seconds = timeout - Math.floor(timeout / 60) * 60;
return ( ({
<> [value]: acc[value],
{minutes} <Trans>min</Trans> {seconds} <Trans>sec</Trans> summary_fields: { [value]: acc[value], ...nested },
</> ...root
} = acc);
const filtered = {
...root,
summary_fields: {
...nested,
},
};
return filtered;
},
{ ...resource }
); );
return filteredResource;
}
// TODO: When prompting is hooked up, update function
// to filter based on prompt overrides
function partitionPromptDetails(resource, launchConfig) {
const { defaults = {} } = launchConfig;
const overrides = {};
if (launchConfig.ask_credential_on_launch) {
let isEqual;
const defaultCreds = defaults.credentials;
const currentCreds = resource?.summary_fields?.credentials;
if (defaultCreds?.length === currentCreds?.length) {
isEqual = currentCreds.every(cred => {
return defaultCreds.some(item => item.id === cred.id);
});
} else {
isEqual = false;
}
if (!isEqual) {
overrides.credentials = resource?.summary_fields?.credentials;
}
}
if (launchConfig.ask_diff_mode_on_launch) {
if (defaults.diff_mode !== resource.diff_mode) {
overrides.diff_mode = resource.diff_mode;
}
}
if (launchConfig.ask_inventory_on_launch) {
if (defaults.inventory.id !== resource.inventory) {
overrides.inventory = resource?.summary_fields?.inventory;
}
}
if (launchConfig.ask_job_type_on_launch) {
if (defaults.job_type !== resource.job_type) {
overrides.job_type = resource.job_type;
}
}
if (launchConfig.ask_limit_on_launch) {
if (defaults.limit !== resource.limit) {
overrides.limit = resource.limit;
}
}
if (launchConfig.ask_scm_branch_on_launch) {
if (defaults.scm_branch !== resource.scm_branch) {
overrides.scm_branch = resource.scm_branch;
}
}
if (launchConfig.ask_skip_tags_on_launch) {
if (defaults.skip_tags !== resource.skip_tags) {
overrides.skip_tags = resource.skip_tags;
}
}
if (launchConfig.ask_tags_on_launch) {
if (defaults.job_tags !== resource.job_tags) {
overrides.job_tags = resource.job_tags;
}
}
if (launchConfig.ask_variables_on_launch) {
if (defaults.extra_vars !== resource.extra_vars) {
overrides.extra_vars = resource.extra_vars;
}
}
if (launchConfig.ask_verbosity_on_launch) {
if (defaults.verbosity !== resource.verbosity) {
overrides.verbosity = resource.verbosity;
}
}
const withoutOverrides = removeOverrides(resource, overrides);
return [withoutOverrides, overrides];
} }
function PromptDetail({ i18n, resource, launchConfig = {} }) { function PromptDetail({ i18n, resource, launchConfig = {} }) {
const { defaults = {} } = launchConfig;
const VERBOSITY = { const VERBOSITY = {
0: i18n._(t`0 (Normal)`), 0: i18n._(t`0 (Normal)`),
1: i18n._(t`1 (Verbose)`), 1: i18n._(t`1 (Verbose)`),
@@ -57,73 +157,79 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
4: i18n._(t`4 (Connection Debug)`), 4: i18n._(t`4 (Connection Debug)`),
}; };
const [details, overrides] = partitionPromptDetails(resource, launchConfig);
const hasOverrides = Object.keys(overrides).length > 0;
return ( return (
<> <>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={i18n._(t`Name`)} value={resource.name} /> <Detail label={i18n._(t`Name`)} value={details.name} />
<Detail label={i18n._(t`Description`)} value={resource.description} /> <Detail label={i18n._(t`Description`)} value={details.description} />
<Detail <Detail
label={i18n._(t`Type`)} label={i18n._(t`Type`)}
value={resource.unified_job_type || resource.type} value={toTitleCase(details.unified_job_type || details.type)}
/> />
<Detail <Detail
label={i18n._(t`Timeout`)} label={i18n._(t`Timeout`)}
value={formatTimeout(resource?.timeout)} value={formatTimeout(details?.timeout)}
/> />
{resource?.summary_fields?.organization && ( {details?.summary_fields?.organization && (
<Detail <Detail
label={i18n._(t`Organization`)} label={i18n._(t`Organization`)}
value={ value={
<Link <Link
to={`/organizations/${resource?.summary_fields.organization.id}/details`} to={`/organizations/${details?.summary_fields.organization.id}/details`}
> >
{resource?.summary_fields?.organization.name} {details?.summary_fields?.organization.name}
</Link> </Link>
} }
/> />
)} )}
{/* TODO: Add JT, WFJT, Inventory Source Details */} {/* TODO: Add JT, WFJT, Inventory Source Details */}
{resource?.type === 'project' && ( {details?.type === 'project' && (
<PromptProjectDetail resource={resource} /> <PromptProjectDetail resource={details} />
)} )}
{resource?.type === 'inventory_source' && ( {details?.type === 'inventory_source' && (
<PromptInventorySourceDetail resource={resource} /> <PromptInventorySourceDetail resource={details} />
)} )}
{resource?.type === 'job_template' && ( {details?.type === 'job_template' && (
<PromptJobTemplateDetail resource={resource} /> <PromptJobTemplateDetail resource={details} />
)} )}
{resource?.type === 'workflow_job_template' && ( {details?.type === 'workflow_job_template' && (
<PromptWFJobTemplateDetail resource={resource} /> <PromptWFJobTemplateDetail resource={details} />
)} )}
<UserDateDetail <UserDateDetail
label={i18n._(t`Created`)} label={i18n._(t`Created`)}
date={resource?.created} date={details?.created}
user={resource?.summary_fields?.created_by} user={details?.summary_fields?.created_by}
/> />
<UserDateDetail <UserDateDetail
label={i18n._(t`Last Modified`)} label={i18n._(t`Last Modified`)}
date={resource?.modified} date={details?.modified}
user={resource?.summary_fields?.modified_by} user={details?.summary_fields?.modified_by}
/> />
</DetailList> </DetailList>
{hasPromptData(launchConfig) && ( {hasPromptData(launchConfig) && hasOverrides && (
<> <>
<PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader> <PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader>
<DetailList> <DetailList aria-label="Prompt Overrides">
{launchConfig.ask_job_type_on_launch && ( {overrides?.job_type && (
<Detail label={i18n._(t`Job Type`)} value={defaults?.job_type} /> <Detail
label={i18n._(t`Job Type`)}
value={toTitleCase(overrides.job_type)}
/>
)} )}
{launchConfig.ask_credential_on_launch && ( {overrides?.credentials && (
<Detail <Detail
fullWidth fullWidth
label={i18n._(t`Credential`)} label={i18n._(t`Credential`)}
rows={4} rows={4}
value={ value={
<ChipGroup numChips={5}> <ChipGroup numChips={5}>
{defaults?.credentials.map(cred => ( {overrides.credentials.map(cred => (
<Chip key={cred.id} isReadOnly> <Chip key={cred.id} isReadOnly>
{cred.name} {cred.name}
</Chip> </Chip>
@@ -132,34 +238,34 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
} }
/> />
)} )}
{launchConfig.ask_inventory_on_launch && ( {overrides?.inventory && (
<Detail <Detail
label={i18n._(t`Inventory`)} label={i18n._(t`Inventory`)}
value={defaults?.inventory?.name} value={overrides.inventory?.name}
/> />
)} )}
{launchConfig.ask_scm_branch_on_launch && ( {overrides?.scm_branch && (
<Detail <Detail
label={i18n._(t`Source Control Branch`)} label={i18n._(t`Source Control Branch`)}
value={defaults?.scm_branch} value={overrides.scm_branch}
/> />
)} )}
{launchConfig.ask_limit_on_launch && ( {overrides?.limit && (
<Detail label={i18n._(t`Limit`)} value={defaults?.limit} /> <Detail label={i18n._(t`Limit`)} value={overrides.limit} />
)} )}
{launchConfig.ask_verbosity_on_launch && ( {overrides?.verbosity && (
<Detail <Detail
label={i18n._(t`Verbosity`)} label={i18n._(t`Verbosity`)}
value={VERBOSITY[(defaults?.verbosity)]} value={VERBOSITY[overrides.verbosity]}
/> />
)} )}
{launchConfig.ask_tags_on_launch && ( {overrides?.job_tags && (
<Detail <Detail
fullWidth fullWidth
label={i18n._(t`Job Tags`)} label={i18n._(t`Job Tags`)}
value={ value={
<ChipGroup numChips={5}> <ChipGroup numChips={5}>
{defaults?.job_tags.split(',').map(jobTag => ( {overrides.job_tags.split(',').map(jobTag => (
<Chip key={jobTag} isReadOnly> <Chip key={jobTag} isReadOnly>
{jobTag} {jobTag}
</Chip> </Chip>
@@ -168,13 +274,13 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
} }
/> />
)} )}
{launchConfig.ask_skip_tags_on_launch && ( {overrides?.skip_tags && (
<Detail <Detail
fullWidth fullWidth
label={i18n._(t`Skip Tags`)} label={i18n._(t`Skip Tags`)}
value={ value={
<ChipGroup numChips={5}> <ChipGroup numChips={5}>
{defaults?.skip_tags.split(',').map(skipTag => ( {overrides.skip_tags.split(',').map(skipTag => (
<Chip key={skipTag} isReadOnly> <Chip key={skipTag} isReadOnly>
{skipTag} {skipTag}
</Chip> </Chip>
@@ -183,19 +289,19 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) {
} }
/> />
)} )}
{launchConfig.ask_diff_mode_on_launch && ( {overrides?.diff_mode && (
<Detail <Detail
label={i18n._(t`Diff Mode`)} label={i18n._(t`Show Changes`)}
value={ value={
defaults?.diff_mode === true ? i18n._(t`On`) : i18n._(t`Off`) overrides.diff_mode === true ? i18n._(t`On`) : i18n._(t`Off`)
} }
/> />
)} )}
{launchConfig.ask_variables_on_launch && ( {overrides?.extra_vars && (
<VariablesDetail <VariablesDetail
label={i18n._(t`Variables`)} label={i18n._(t`Variables`)}
rows={4} rows={4}
value={defaults?.extra_vars} value={overrides.extra_vars}
/> />
)} )}
</DetailList> </DetailList>

View File

@@ -1,16 +1,9 @@
import React from 'react'; import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import mockTemplate from './data.job_template.json';
import PromptDetail from './PromptDetail'; import PromptDetail from './PromptDetail';
const mockTemplate = {
name: 'Mock Template',
description: 'mock description',
unified_job_type: 'job',
created: '2019-08-08T19:24:05.344276Z',
modified: '2019-08-08T19:24:18.162949Z',
};
const mockPromptLaunch = { const mockPromptLaunch = {
ask_credential_on_launch: true, ask_credential_on_launch: true,
ask_diff_mode_on_launch: true, ask_diff_mode_on_launch: true,
@@ -26,10 +19,10 @@ const mockPromptLaunch = {
extra_vars: '---foo: bar', extra_vars: '---foo: bar',
diff_mode: false, diff_mode: false,
limit: 3, limit: 3,
job_tags: 'one,two,three', job_tags: 'T_100,T_200',
skip_tags: 'skip', skip_tags: 'S_100,S_200',
job_type: 'run', job_type: 'run',
verbosity: 1, verbosity: 3,
inventory: { inventory: {
name: 'Demo Inventory', name: 'Demo Inventory',
id: 1, id: 1,
@@ -37,12 +30,16 @@ const mockPromptLaunch = {
credentials: [ credentials: [
{ {
id: 1, id: 1,
name: 'Demo Credential', kind: 'ssh',
credential_type: 1, name: 'Credential 1',
passwords_needed: [], },
{
id: 2,
kind: 'awx',
name: 'Credential 2',
}, },
], ],
scm_branch: '123', scm_branch: 'Foo branch',
}, },
}; };
@@ -71,21 +68,40 @@ describe('PromptDetail', () => {
} }
expect(wrapper.find('PromptDetail h2').text()).toBe('Prompted Values'); expect(wrapper.find('PromptDetail h2').text()).toBe('Prompted Values');
assertDetail('Name', 'Mock Template'); assertDetail('Name', 'Mock JT');
assertDetail('Description', 'mock description'); assertDetail('Description', 'Mock JT Description');
assertDetail('Type', 'job'); assertDetail('Type', 'Job Template');
assertDetail('Job Type', 'run'); assertDetail('Job Type', 'Run');
assertDetail('Credential', 'Demo Credential');
assertDetail('Inventory', 'Demo Inventory'); assertDetail('Inventory', 'Demo Inventory');
assertDetail('Source Control Branch', '123'); assertDetail('Source Control Branch', 'Foo branch');
assertDetail('Limit', '3'); assertDetail('Limit', 'alpha:beta');
assertDetail('Verbosity', '1 (Verbose)'); assertDetail('Verbosity', '3 (Debug)');
assertDetail('Job Tags', 'onetwothree'); assertDetail('Show Changes', 'Off');
assertDetail('Skip Tags', 'skip');
assertDetail('Diff Mode', 'Off');
expect(wrapper.find('VariablesDetail').prop('value')).toEqual( expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
'---foo: bar' '---foo: bar'
); );
expect(
wrapper
.find('Detail[label="Credentials"]')
.containsAllMatchingElements([
<span>
<strong>SSH:</strong>Credential 1
</span>,
<span>
<strong>Awx:</strong>Credential 2
</span>,
])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Job Tags"]')
.containsAnyMatchingElements([<span>T_100</span>, <span>T_200</span>])
).toEqual(true);
expect(
wrapper
.find('Detail[label="Skip Tags"]')
.containsAllMatchingElements([<span>S_100</span>, <span>S_200</span>])
).toEqual(true);
}); });
}); });
@@ -106,8 +122,11 @@ describe('PromptDetail', () => {
}); });
test('should not render promptable details', () => { test('should not render promptable details', () => {
const overrideDetails = wrapper.find(
'DetailList[aria-label="Prompt Overrides"]'
);
function assertNoDetail(label) { function assertNoDetail(label) {
expect(wrapper.find(`Detail[label="${label}"]`).length).toBe(0); expect(overrideDetails.find(`Detail[label="${label}"]`).length).toBe(0);
} }
[ [
'Job Type', 'Job Type',
@@ -120,8 +139,8 @@ describe('PromptDetail', () => {
'Skip Tags', 'Skip Tags',
'Diff Mode', 'Diff Mode',
].forEach(label => assertNoDetail(label)); ].forEach(label => assertNoDetail(label));
expect(wrapper.find('PromptDetail h2').length).toBe(0); expect(overrideDetails.find('PromptDetail h2').length).toBe(0);
expect(wrapper.find('VariablesDetail').length).toBe(0); expect(overrideDetails.find('VariablesDetail').length).toBe(0);
}); });
}); });
}); });

View File

@@ -7,6 +7,8 @@ import { Chip, ChipGroup, List, ListItem } from '@patternfly/react-core';
import { Detail } from '@components/DetailList'; import { Detail } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput'; import { VariablesDetail } from '@components/CodeMirrorInput';
import CredentialChip from '@components/CredentialChip'; import CredentialChip from '@components/CredentialChip';
import Sparkline from '@components/Sparkline';
import { toTitleCase } from '@util/strings';
function PromptJobTemplateDetail({ i18n, resource }) { function PromptJobTemplateDetail({ i18n, resource }) {
const { const {
@@ -61,14 +63,32 @@ function PromptJobTemplateDetail({ i18n, resource }) {
); );
} }
const inventoryKind =
summary_fields?.inventory?.kind === 'smart'
? 'smart_inventory'
: 'inventory';
const recentJobs = summary_fields.recent_jobs.map(job => ({
...job,
type: 'job',
}));
return ( return (
<> <>
<Detail label={i18n._(t`Job Type`)} value={job_type} /> {summary_fields.recent_jobs?.length > 0 && (
<Detail
value={<Sparkline jobs={recentJobs} />}
label={i18n._(t`Activity`)}
/>
)}
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(job_type)} />
{summary_fields?.inventory && ( {summary_fields?.inventory && (
<Detail <Detail
label={i18n._(t`Inventory`)} label={i18n._(t`Inventory`)}
value={ value={
<Link to={`/inventories/${summary_fields.inventory?.id}/details`}> <Link
to={`/${inventoryKind}/${summary_fields.inventory?.id}/details`}
>
{summary_fields.inventory?.name} {summary_fields.inventory?.name}
</Link> </Link>
} }
@@ -84,7 +104,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
} }
/> />
)} )}
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} /> <Detail label={i18n._(t`Source Control Branch`)} value={scm_branch} />
<Detail label={i18n._(t`Playbook`)} value={playbook} /> <Detail label={i18n._(t`Playbook`)} value={playbook} />
<Detail label={i18n._(t`Forks`)} value={forks || '0'} /> <Detail label={i18n._(t`Forks`)} value={forks || '0'} />
<Detail label={i18n._(t`Limit`)} value={limit} /> <Detail label={i18n._(t`Limit`)} value={limit} />
@@ -103,6 +123,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
/> />
</React.Fragment> </React.Fragment>
)} )}
{optionsList && <Detail label={i18n._(t`Options`)} value={optionsList} />}
{summary_fields?.credentials?.length > 0 && ( {summary_fields?.credentials?.length > 0 && (
<Detail <Detail
fullWidth fullWidth
@@ -172,7 +193,6 @@ function PromptJobTemplateDetail({ i18n, resource }) {
} }
/> />
)} )}
{optionsList && <Detail label={i18n._(t`Options`)} value={optionsList} />}
{extra_vars && ( {extra_vars && (
<VariablesDetail <VariablesDetail
label={i18n._(t`Variables`)} label={i18n._(t`Variables`)}

View File

@@ -38,10 +38,10 @@ describe('PromptJobTemplateDetail', () => {
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
} }
assertDetail('Job Type', 'run'); assertDetail('Job Type', 'Run');
assertDetail('Inventory', 'Demo Inventory'); assertDetail('Inventory', 'Demo Inventory');
assertDetail('Project', 'Mock Project'); assertDetail('Project', 'Mock Project');
assertDetail('SCM Branch', 'Foo branch'); assertDetail('Source Control Branch', 'Foo branch');
assertDetail('Playbook', 'ping.yml'); assertDetail('Playbook', 'ping.yml');
assertDetail('Forks', '2'); assertDetail('Forks', '2');
assertDetail('Limit', 'alpha:beta'); assertDetail('Limit', 'alpha:beta');