diff --git a/awx/ui_next/src/components/DetailList/DeletedDetail.jsx b/awx/ui_next/src/components/DetailList/DeletedDetail.jsx new file mode 100644 index 0000000000..e71fc183bd --- /dev/null +++ b/awx/ui_next/src/components/DetailList/DeletedDetail.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { node } from 'prop-types'; +import styled from 'styled-components'; +import _Detail from './Detail'; + +const Detail = styled(_Detail)` + dd& { + color: red; + } +`; + +function DeletedDetail({ i18n, label }) { + return ; +} + +DeletedDetail.propTypes = { + label: node.isRequired, +}; + +export default withI18n()(DeletedDetail); diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index 4a5b77dbc0..8573f1a614 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -1,3 +1,4 @@ export { default as DetailList } from './DetailList'; export { default as Detail, DetailName, DetailValue } from './Detail'; +export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; diff --git a/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx index e2aa73ec6e..05ff230175 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptDetail.jsx @@ -34,6 +34,21 @@ function formatTimeout(timeout) { ); } +function buildResourceLink(resource) { + const link = { + job_template: `/templates/job_template/${resource.id}/details`, + project: `/projects/${resource.id}/details`, + inventory_source: `/inventories/inventory/${resource.inventory}/sources/${resource.id}/details`, + workflow_job_template: `/templates/workflow_job_template/${resource.id}/details`, + }; + + return link[(resource?.type)] ? ( + {resource.name} + ) : ( + resource.name + ); +} + function hasPromptData(launchData) { return ( launchData.ask_credential_on_launch || @@ -150,7 +165,7 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) { return ( <> - + - {details?.summary_fields?.organization && ( - - {details?.summary_fields?.organization.name} - - } - /> - )} - {details?.type === 'project' && ( )} @@ -185,17 +187,20 @@ function PromptDetail({ i18n, resource, launchConfig = {} }) { {details?.type === 'workflow_job_template' && ( )} - - - + {details?.created && ( + + )} + {details?.modified && ( + + )} {hasPromptData(launchConfig) && hasOverrides && ( diff --git a/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx index 6b385c5ef3..f3ec46c003 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx @@ -4,7 +4,7 @@ import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; import { Chip, ChipGroup, List, ListItem } from '@patternfly/react-core'; -import { Detail } from '@components/DetailList'; +import { Detail, DeletedDetail } from '@components/DetailList'; import { VariablesDetail } from '@components/CodeMirrorInput'; import CredentialChip from '@components/CredentialChip'; @@ -57,6 +57,20 @@ function PromptInventorySourceDetail({ i18n, resource }) { return ( <> + {summary_fields?.organization ? ( + + {summary_fields?.organization.name} + + } + /> + ) : ( + + )} {summary_fields?.inventory && ( { let wrapper; @@ -21,18 +26,13 @@ describe('PromptInventorySourceDetail', () => { }); test('should render expected details', () => { - function assertDetail(label, value) { - expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label); - expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); - } - - assertDetail('Inventory', 'Demo Inventory'); - assertDetail('Source', 'scm'); - assertDetail('Project', 'Mock Project'); - assertDetail('Inventory File', 'foo'); - assertDetail('Custom Inventory Script', 'Mock Script'); - assertDetail('Verbosity', '2 (More Verbose)'); - assertDetail('Cache Timeout', '2 Seconds'); + assertDetail(wrapper, 'Inventory', 'Demo Inventory'); + assertDetail(wrapper, 'Source', 'scm'); + assertDetail(wrapper, 'Project', 'Mock Project'); + assertDetail(wrapper, 'Inventory File', 'foo'); + assertDetail(wrapper, 'Custom Inventory Script', 'Mock Script'); + assertDetail(wrapper, 'Verbosity', '2 (More Verbose)'); + assertDetail(wrapper, 'Cache Timeout', '2 Seconds'); expect( wrapper .find('Detail[label="Regions"]') @@ -74,4 +74,12 @@ describe('PromptInventorySourceDetail', () => { ]) ).toEqual(true); }); + + test('should render "Deleted" details', () => { + delete mockInvSource.summary_fields.organization; + wrapper = mountWithContexts( + + ); + assertDetail(wrapper, 'Organization', 'Deleted'); + }); }); diff --git a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx index e8f979247b..d7502bb942 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.jsx @@ -4,7 +4,7 @@ import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; import { Chip, ChipGroup, List, ListItem } from '@patternfly/react-core'; -import { Detail } from '@components/DetailList'; +import { Detail, DeletedDetail } from '@components/DetailList'; import { VariablesDetail } from '@components/CodeMirrorInput'; import CredentialChip from '@components/CredentialChip'; import Sparkline from '@components/Sparkline'; @@ -13,6 +13,7 @@ import { toTitleCase } from '@util/strings'; function PromptJobTemplateDetail({ i18n, resource }) { const { allow_simultaneous, + ask_inventory_on_launch, become_enabled, diff_mode, extra_vars, @@ -84,7 +85,21 @@ function PromptJobTemplateDetail({ i18n, resource }) { /> )} - {summary_fields?.inventory && ( + {summary_fields?.organization ? ( + + {summary_fields?.organization.name} + + } + /> + ) : ( + + )} + {summary_fields?.inventory ? ( } /> + ) : ( + !ask_inventory_on_launch && ( + + ) )} - {summary_fields?.project && ( + {summary_fields?.project ? ( } /> + ) : ( + )} diff --git a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx index 30f92824ab..7a536f4b73 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptJobTemplateDetail.test.jsx @@ -18,6 +18,11 @@ const mockJT = { ], }; +function assertDetail(wrapper, label, value) { + expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label); + expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); +} + describe('PromptJobTemplateDetail', () => { let wrapper; @@ -34,24 +39,19 @@ describe('PromptJobTemplateDetail', () => { }); test('should render expected details', () => { - function assertDetail(label, value) { - expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label); - expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); - } - - assertDetail('Job Type', 'Run'); - assertDetail('Inventory', 'Demo Inventory'); - assertDetail('Project', 'Mock Project'); - assertDetail('Source Control Branch', 'Foo branch'); - assertDetail('Playbook', 'ping.yml'); - assertDetail('Forks', '2'); - assertDetail('Limit', 'alpha:beta'); - assertDetail('Verbosity', '3 (Debug)'); - assertDetail('Show Changes', 'Off'); - assertDetail('Job Slicing', '1'); - assertDetail('Host Config Key', 'a1b2c3'); - assertDetail('Webhook Service', 'Github'); - assertDetail('Webhook Key', 'PiM3n2'); + assertDetail(wrapper, 'Job Type', 'Run'); + assertDetail(wrapper, 'Inventory', 'Demo Inventory'); + assertDetail(wrapper, 'Project', 'Mock Project'); + assertDetail(wrapper, 'Source Control Branch', 'Foo branch'); + assertDetail(wrapper, 'Playbook', 'ping.yml'); + assertDetail(wrapper, 'Forks', '2'); + assertDetail(wrapper, 'Limit', 'alpha:beta'); + assertDetail(wrapper, 'Verbosity', '3 (Debug)'); + assertDetail(wrapper, 'Show Changes', 'Off'); + assertDetail(wrapper, 'Job Slicing', '1'); + assertDetail(wrapper, 'Host Config Key', 'a1b2c3'); + assertDetail(wrapper, 'Webhook Service', 'Github'); + assertDetail(wrapper, 'Webhook Key', 'PiM3n2'); expect(wrapper.find('StatusIcon')).toHaveLength(2); expect(wrapper.find('Detail[label="Webhook URL"] dd').text()).toEqual( expect.stringContaining('/api/v2/job_templates/7/github/') @@ -112,4 +112,16 @@ describe('PromptJobTemplateDetail', () => { '---foo: bar' ); }); + + test('should render "Deleted" details', () => { + delete mockJT.summary_fields.inventory; + delete mockJT.summary_fields.organization; + delete mockJT.summary_fields.project; + + wrapper = mountWithContexts(); + + assertDetail(wrapper, 'Inventory', 'Deleted'); + assertDetail(wrapper, 'Organization', 'Deleted'); + assertDetail(wrapper, 'Project', 'Deleted'); + }); }); diff --git a/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx index 577a706d7d..b783631d14 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx @@ -3,8 +3,9 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '@contexts/Config'; import { List, ListItem } from '@patternfly/react-core'; +import { Link } from 'react-router-dom'; -import { Detail } from '@components/DetailList'; +import { Detail, DeletedDetail } from '@components/DetailList'; import CredentialChip from '@components/CredentialChip'; import { toTitleCase } from '@util/strings'; @@ -49,6 +50,20 @@ function PromptProjectDetail({ i18n, resource }) { return ( <> + {summary_fields?.organization ? ( + + {summary_fields?.organization.name} + + } + /> + ) : ( + + )} { let wrapper; + const config = { + project_base_dir: 'dir/foo/bar', + }; beforeAll(() => { - const config = { - project_base_dir: 'dir/foo/bar', - }; wrapper = mountWithContexts( , { @@ -27,23 +32,19 @@ describe('PromptProjectDetail', () => { }); test('should render expected details', () => { - function assertDetail(label, value) { - expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label); - expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value); - } - - assertDetail('Source Control Type', 'Git'); + assertDetail(wrapper, 'Source Control Type', 'Git'); assertDetail( + wrapper, 'Source Control URL', 'https://github.com/ansible/ansible-tower-samples' ); - assertDetail('Source Control Branch', 'foo'); - assertDetail('Source Control Refspec', 'refs/'); - assertDetail('Cache Timeout', '3 Seconds'); - assertDetail('Ansible Environment', 'mock virtual env'); - assertDetail('Project Base Path', 'dir/foo/bar'); - assertDetail('Playbook Directory', '_6__demo_project'); - assertDetail('Source Control Credential', 'Scm: mock scm'); + assertDetail(wrapper, 'Source Control Branch', 'foo'); + assertDetail(wrapper, 'Source Control Refspec', 'refs/'); + assertDetail(wrapper, 'Cache Timeout', '3 Seconds'); + assertDetail(wrapper, 'Ansible Environment', 'mock virtual env'); + assertDetail(wrapper, 'Project Base Path', 'dir/foo/bar'); + assertDetail(wrapper, 'Playbook Directory', '_6__demo_project'); + assertDetail(wrapper, 'Source Control Credential', 'Scm: mock scm'); expect( wrapper .find('Detail[label="Options"]') @@ -55,4 +56,15 @@ describe('PromptProjectDetail', () => { ]) ).toEqual(true); }); + + test('should render "Deleted" details', () => { + delete mockProject.summary_fields.organization; + wrapper = mountWithContexts( + , + { + context: { config }, + } + ); + assertDetail(wrapper, 'Organization', 'Deleted'); + }); }); diff --git a/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx index dd38a766bd..c823b34184 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptWFJobTemplateDetail.jsx @@ -52,6 +52,18 @@ function PromptWFJobTemplateDetail({ i18n, resource }) { label={i18n._(t`Activity`)} /> )} + {summary_fields?.organization && ( + + {summary_fields?.organization.name} + + } + /> + )} {summary_fields?.inventory && ( ); - const renderMissingDataDetail = value => ( - - ); - const inventoryValue = (kind, id) => { const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory'; @@ -180,7 +174,7 @@ function JobTemplateDetail({ i18n, template }) { } /> ) : ( - renderMissingDataDetail(i18n._(t`Project`)) + )} {summary_fields.inventory ? ( ) : ( - !ask_inventory_on_launch && - renderMissingDataDetail(i18n._(t`Inventory`)) + !ask_inventory_on_launch && ( + + ) )} {summary_fields.project ? ( ) : ( - renderMissingDataDetail(i18n._(t`Project`)) + )}