diff --git a/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx index 6ea976a0b5..2aafad7259 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx @@ -4,6 +4,7 @@ import { withI18n } from '@lingui/react'; import { t, Trans } from '@lingui/macro'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; +import { toTitleCase } from '@util/strings'; import { Chip, ChipGroup } from '@patternfly/react-core'; import { VariablesDetail } from '@components/CodeMirrorInput'; @@ -19,6 +20,19 @@ const PromptHeader = styled.h2` 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} min {seconds} sec + + ); +} + function hasPromptData(launchData) { return ( launchData.ask_credential_on_launch || @@ -34,21 +48,107 @@ function hasPromptData(launchData) { ); } -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} min {seconds} sec - +function removeOverrides(resource, overrides) { + const filteredResource = Object.keys(overrides).reduce( + (acc, value) => { + let root; + let nested; + + ({ + [value]: acc[value], + 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 = {} }) { - const { defaults = {} } = launchConfig; const VERBOSITY = { 0: i18n._(t`0 (Normal)`), 1: i18n._(t`1 (Verbose)`), @@ -57,73 +157,79 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) { 4: i18n._(t`4 (Connection Debug)`), }; + const [details, overrides] = partitionPromptDetails(resource, launchConfig); + const hasOverrides = Object.keys(overrides).length > 0; + return ( <> - - + + - {resource?.summary_fields?.organization && ( + {details?.summary_fields?.organization && ( - {resource?.summary_fields?.organization.name} + {details?.summary_fields?.organization.name} } /> )} {/* TODO: Add JT, WFJT, Inventory Source Details */} - {resource?.type === 'project' && ( - + {details?.type === 'project' && ( + )} - {resource?.type === 'inventory_source' && ( - + {details?.type === 'inventory_source' && ( + )} - {resource?.type === 'job_template' && ( - + {details?.type === 'job_template' && ( + )} - {resource?.type === 'workflow_job_template' && ( - + {details?.type === 'workflow_job_template' && ( + )} - {hasPromptData(launchConfig) && ( + {hasPromptData(launchConfig) && hasOverrides && ( <> {i18n._(t`Prompted Values`)} - - {launchConfig.ask_job_type_on_launch && ( - + + {overrides?.job_type && ( + )} - {launchConfig.ask_credential_on_launch && ( + {overrides?.credentials && ( - {defaults?.credentials.map(cred => ( + {overrides.credentials.map(cred => ( {cred.name} @@ -132,34 +238,34 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) { } /> )} - {launchConfig.ask_inventory_on_launch && ( + {overrides?.inventory && ( )} - {launchConfig.ask_scm_branch_on_launch && ( + {overrides?.scm_branch && ( )} - {launchConfig.ask_limit_on_launch && ( - + {overrides?.limit && ( + )} - {launchConfig.ask_verbosity_on_launch && ( + {overrides?.verbosity && ( )} - {launchConfig.ask_tags_on_launch && ( + {overrides?.job_tags && ( - {defaults?.job_tags.split(',').map(jobTag => ( + {overrides.job_tags.split(',').map(jobTag => ( {jobTag} @@ -168,13 +274,13 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) { } /> )} - {launchConfig.ask_skip_tags_on_launch && ( + {overrides?.skip_tags && ( - {defaults?.skip_tags.split(',').map(skipTag => ( + {overrides.skip_tags.split(',').map(skipTag => ( {skipTag} @@ -183,19 +289,19 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) { } /> )} - {launchConfig.ask_diff_mode_on_launch && ( + {overrides?.diff_mode && ( )} - {launchConfig.ask_variables_on_launch && ( + {overrides?.extra_vars && ( )} diff --git a/awx/ui_next/src/components/PromptDetail/PromptDetail.test.jsx b/awx/ui_next/src/components/PromptDetail/PromptDetail.test.jsx index 265d1b227c..17ecfd4e30 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptDetail.test.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptDetail.test.jsx @@ -1,16 +1,9 @@ import React from 'react'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import mockTemplate from './data.job_template.json'; 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 = { ask_credential_on_launch: true, ask_diff_mode_on_launch: true, @@ -26,10 +19,10 @@ const mockPromptLaunch = { extra_vars: '---foo: bar', diff_mode: false, limit: 3, - job_tags: 'one,two,three', - skip_tags: 'skip', + job_tags: 'T_100,T_200', + skip_tags: 'S_100,S_200', job_type: 'run', - verbosity: 1, + verbosity: 3, inventory: { name: 'Demo Inventory', id: 1, @@ -37,12 +30,16 @@ const mockPromptLaunch = { credentials: [ { id: 1, - name: 'Demo Credential', - credential_type: 1, - passwords_needed: [], + kind: 'ssh', + name: 'Credential 1', + }, + { + 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'); - assertDetail('Name', 'Mock Template'); - assertDetail('Description', 'mock description'); - assertDetail('Type', 'job'); - assertDetail('Job Type', 'run'); - assertDetail('Credential', 'Demo Credential'); + assertDetail('Name', 'Mock JT'); + assertDetail('Description', 'Mock JT Description'); + assertDetail('Type', 'Job Template'); + assertDetail('Job Type', 'Run'); assertDetail('Inventory', 'Demo Inventory'); - assertDetail('Source Control Branch', '123'); - assertDetail('Limit', '3'); - assertDetail('Verbosity', '1 (Verbose)'); - assertDetail('Job Tags', 'onetwothree'); - assertDetail('Skip Tags', 'skip'); - assertDetail('Diff Mode', 'Off'); + assertDetail('Source Control Branch', 'Foo branch'); + assertDetail('Limit', 'alpha:beta'); + assertDetail('Verbosity', '3 (Debug)'); + assertDetail('Show Changes', 'Off'); expect(wrapper.find('VariablesDetail').prop('value')).toEqual( '---foo: bar' ); + expect( + wrapper + .find('Detail[label="Credentials"]') + .containsAllMatchingElements([ + + SSH:Credential 1 + , + + Awx:Credential 2 + , + ]) + ).toEqual(true); + expect( + wrapper + .find('Detail[label="Job Tags"]') + .containsAnyMatchingElements([T_100, T_200]) + ).toEqual(true); + expect( + wrapper + .find('Detail[label="Skip Tags"]') + .containsAllMatchingElements([S_100, S_200]) + ).toEqual(true); }); }); @@ -106,8 +122,11 @@ describe('PromptDetail', () => { }); test('should not render promptable details', () => { + const overrideDetails = wrapper.find( + 'DetailList[aria-label="Prompt Overrides"]' + ); function assertNoDetail(label) { - expect(wrapper.find(`Detail[label="${label}"]`).length).toBe(0); + expect(overrideDetails.find(`Detail[label="${label}"]`).length).toBe(0); } [ 'Job Type', @@ -120,8 +139,8 @@ describe('PromptDetail', () => { 'Skip Tags', 'Diff Mode', ].forEach(label => assertNoDetail(label)); - expect(wrapper.find('PromptDetail h2').length).toBe(0); - expect(wrapper.find('VariablesDetail').length).toBe(0); + expect(overrideDetails.find('PromptDetail h2').length).toBe(0); + expect(overrideDetails.find('VariablesDetail').length).toBe(0); }); }); }); diff --git a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx index 7c0d555485..bb34806a19 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx @@ -7,6 +7,8 @@ import { Chip, ChipGroup, List, ListItem } from '@patternfly/react-core'; import { Detail } from '@components/DetailList'; import { VariablesDetail } from '@components/CodeMirrorInput'; import CredentialChip from '@components/CredentialChip'; +import Sparkline from '@components/Sparkline'; +import { toTitleCase } from '@util/strings'; function PromptJobTemplateDetail({ i18n, resource }) { 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 ( <> - + {summary_fields.recent_jobs?.length > 0 && ( + } + label={i18n._(t`Activity`)} + /> + )} + {summary_fields?.inventory && ( + {summary_fields.inventory?.name} } @@ -84,7 +104,7 @@ function PromptJobTemplateDetail({ i18n, resource }) { } /> )} - + @@ -103,6 +123,7 @@ function PromptJobTemplateDetail({ i18n, resource }) { /> )} + {optionsList && } {summary_fields?.credentials?.length > 0 && ( )} - {optionsList && } {extra_vars && ( { expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); } - assertDetail('Job Type', 'run'); + assertDetail('Job Type', 'Run'); assertDetail('Inventory', 'Demo Inventory'); assertDetail('Project', 'Mock Project'); - assertDetail('SCM Branch', 'Foo branch'); + assertDetail('Source Control Branch', 'Foo branch'); assertDetail('Playbook', 'ping.yml'); assertDetail('Forks', '2'); assertDetail('Limit', 'alpha:beta');