Enhanced detail component (#12432)

* Enhanced detail component to handle cases with no values, and refactored components that use detail component.

* Add optional chaining operators where necessary to pass test cases

* add test cases to test suites of modified files

Co-authored-by: Veda Periwal <vperiwal@vperiwal-mac.attlocal.net>
This commit is contained in:
vedaperi
2022-07-19 14:17:27 -07:00
committed by GitHub
parent 54057f1c80
commit 71925de902
38 changed files with 954 additions and 313 deletions

View File

@@ -41,6 +41,7 @@ const Detail = ({
className, className,
dataCy, dataCy,
alwaysVisible, alwaysVisible,
isEmpty,
helpText, helpText,
isEncrypted, isEncrypted,
isNotConfigured, isNotConfigured,
@@ -49,6 +50,10 @@ const Detail = ({
return null; return null;
} }
if (isEmpty && !alwaysVisible) {
return null;
}
const labelCy = dataCy ? `${dataCy}-label` : null; const labelCy = dataCy ? `${dataCy}-label` : null;
const valueCy = dataCy ? `${dataCy}-value` : null; const valueCy = dataCy ? `${dataCy}-value` : null;

View File

@@ -163,16 +163,16 @@ function JobListItem({
<Td colSpan={showTypeColumn ? 6 : 5}> <Td colSpan={showTypeColumn ? 6 : 5}>
<ExpandableRowContent> <ExpandableRowContent>
<DetailList> <DetailList>
{job.type === 'inventory_update' && {job.type === 'inventory_update' && (
inventorySourceLabels.length > 0 && ( <Detail
<Detail dataCy="job-inventory-source-type"
dataCy="job-inventory-source-type" label={t`Source`}
label={t`Source`} value={inventorySourceLabels?.map(([string, label]) =>
value={inventorySourceLabels.map(([string, label]) => string === job.source ? label : null
string === job.source ? label : null )}
)} isEmpty={inventorySourceLabels?.length === 0}
/> />
)} )}
<LaunchedByDetail job={job} /> <LaunchedByDetail job={job} />
{job.launch_type === 'scheduled' && {job.launch_type === 'scheduled' &&
(schedule ? ( (schedule ? (
@@ -254,7 +254,7 @@ function JobListItem({
dataCy={`execution-environment-detail-${job.id}`} dataCy={`execution-environment-detail-${job.id}`}
/> />
)} )}
{credentials && credentials.length > 0 && ( {credentials && (
<Detail <Detail
fullWidth fullWidth
label={t`Credentials`} label={t`Credentials`}
@@ -275,6 +275,7 @@ function JobListItem({
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={credentials.length === 0}
/> />
)} )}
{labels && labels.count > 0 && ( {labels && labels.count > 0 && (

View File

@@ -203,6 +203,49 @@ describe('<JobListItem />', () => {
wrapper.find('Detail[label="Execution Environment"] dd').text() wrapper.find('Detail[label="Execution Environment"] dd').text()
).toBe('Missing resource'); ).toBe('Missing resource');
}); });
test('should not load Source', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
inventorySourceLabels={[]}
job={{
...mockJob,
type: 'inventory_update',
summary_fields: {
user_capabilities: {},
},
}}
/>
</tbody>
</table>
);
const source_detail = wrapper.find(`Detail[label="Source"]`).at(0);
expect(source_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Credentials', () => {
wrapper = mountWithContexts(
<table>
<tbody>
<JobListItem
job={{
...mockJob,
type: 'inventory_update',
summary_fields: {
credentials: [],
},
}}
/>
</tbody>
</table>
);
const credentials_detail = wrapper
.find(`Detail[label="Credentials"]`)
.at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
}); });
describe('<JobListItem with failed job />', () => { describe('<JobListItem with failed job />', () => {

View File

@@ -113,15 +113,14 @@ function PromptInventorySourceDetail({ resource }) {
label={t`Cache Timeout`} label={t`Cache Timeout`}
value={`${update_cache_timeout} ${t`Seconds`}`} value={`${update_cache_timeout} ${t`Seconds`}`}
/> />
{summary_fields?.credentials?.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Credential`}
label={t`Credential`} value={summary_fields?.credentials?.map((cred) => (
value={summary_fields.credentials.map((cred) => ( <CredentialChip key={cred?.id} credential={cred} isReadOnly />
<CredentialChip key={cred?.id} credential={cred} isReadOnly /> ))}
))} isEmpty={summary_fields?.credentials?.length === 0}
/> />
)}
{source_regions && ( {source_regions && (
<Detail <Detail
fullWidth fullWidth

View File

@@ -79,4 +79,19 @@ describe('PromptInventorySourceDetail', () => {
); );
assertDetail(wrapper, 'Organization', 'Deleted'); assertDetail(wrapper, 'Organization', 'Deleted');
}); });
test('should not load Credentials', () => {
wrapper = mountWithContexts(
<PromptInventorySourceDetail
resource={{
...mockInvSource,
summary_fields: {
credentials: [],
},
}}
/>
);
const credentials_detail = wrapper.find(`Detail[label="Credential"]`).at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -26,7 +26,7 @@ function PromptJobTemplateDetail({ resource }) {
extra_vars, extra_vars,
forks, forks,
host_config_key, host_config_key,
instance_groups, instance_groups = [],
job_slice_count, job_slice_count,
job_tags, job_tags,
job_type, job_type,
@@ -94,9 +94,11 @@ function PromptJobTemplateDetail({ resource }) {
return ( return (
<> <>
{summary_fields.recent_jobs?.length > 0 && ( <Detail
<Detail value={<Sparkline jobs={recentJobs} />} label={t`Activity`} /> label={t`Activity`}
)} value={<Sparkline jobs={recentJobs} />}
isEmpty={summary_fields.recent_jobs?.length === 0}
/>
<Detail label={t`Job Type`} value={toTitleCase(job_type)} /> <Detail label={t`Job Type`} value={toTitleCase(job_type)} />
{summary_fields?.organization ? ( {summary_fields?.organization ? (
<Detail <Detail
@@ -180,7 +182,7 @@ function PromptJobTemplateDetail({ resource }) {
/> />
)} )}
{optionsList && <Detail label={t`Enabled Options`} value={optionsList} />} {optionsList && <Detail label={t`Enabled Options`} value={optionsList} />}
{summary_fields?.credentials?.length > 0 && ( {summary_fields?.credentials && (
<Detail <Detail
fullWidth fullWidth
label={t`Credentials`} label={t`Credentials`}
@@ -195,9 +197,10 @@ function PromptJobTemplateDetail({ resource }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={summary_fields?.credentials?.length === 0}
/> />
)} )}
{summary_fields?.labels?.results?.length > 0 && ( {summary_fields?.labels?.results && (
<Detail <Detail
fullWidth fullWidth
label={t`Labels`} label={t`Labels`}
@@ -214,28 +217,28 @@ function PromptJobTemplateDetail({ resource }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={summary_fields?.labels?.results?.length === 0}
/> />
)} )}
{instance_groups?.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Instance Groups`}
label={t`Instance Groups`} value={
value={ <ChipGroup
<ChipGroup numChips={5}
numChips={5} totalChips={instance_groups?.length}
totalChips={instance_groups.length} ouiaId="prompt-jt-instance-group-chips"
ouiaId="prompt-jt-instance-group-chips" >
> {instance_groups?.map((ig) => (
{instance_groups.map((ig) => ( <Chip key={ig.id} isReadOnly>
<Chip key={ig.id} isReadOnly> {ig.name}
{ig.name} </Chip>
</Chip> ))}
))} </ChipGroup>
</ChipGroup> }
} isEmpty={instance_groups?.length === 0}
/> />
)} {job_tags && (
{job_tags?.length > 0 && (
<Detail <Detail
fullWidth fullWidth
label={t`Job Tags`} label={t`Job Tags`}
@@ -252,9 +255,10 @@ function PromptJobTemplateDetail({ resource }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={job_tags?.length === 0}
/> />
)} )}
{skip_tags?.length > 0 && ( {skip_tags && (
<Detail <Detail
fullWidth fullWidth
label={t`Skip Tags`} label={t`Skip Tags`}
@@ -271,6 +275,7 @@ function PromptJobTemplateDetail({ resource }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={skip_tags?.length === 0}
/> />
)} )}
{extra_vars && ( {extra_vars && (

View File

@@ -125,4 +125,92 @@ describe('PromptJobTemplateDetail', () => {
assertDetail(wrapper, 'Organization', 'Deleted'); assertDetail(wrapper, 'Organization', 'Deleted');
assertDetail(wrapper, 'Project', 'Deleted'); assertDetail(wrapper, 'Project', 'Deleted');
}); });
test('should not load Activity', () => {
wrapper = mountWithContexts(
<PromptJobTemplateDetail
resource={{
...mockJT,
summary_fields: {
recent_jobs: [],
},
}}
/>
);
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Credentials', () => {
wrapper = mountWithContexts(
<PromptJobTemplateDetail
resource={{
...mockJT,
summary_fields: {
credentials: [],
},
}}
/>
);
const credentials_detail = wrapper
.find(`Detail[label="Credentials"]`)
.at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Labels', () => {
wrapper = mountWithContexts(
<PromptJobTemplateDetail
resource={{
...mockJT,
summary_fields: {
labels: {
results: [],
},
},
}}
/>
);
const labels_detail = wrapper.find(`Detail[label="Labels"]`).at(0);
expect(labels_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Instance Groups', () => {
wrapper = mountWithContexts(
<PromptJobTemplateDetail
resource={{
...mockJT,
instance_groups: [],
}}
/>
);
const instance_groups_detail = wrapper
.find(`Detail[label="Instance Groups"]`)
.at(0);
expect(instance_groups_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Job Tags', () => {
wrapper = mountWithContexts(
<PromptJobTemplateDetail
resource={{
...mockJT,
job_tags: '',
}}
/>
);
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0);
});
test('should not load Skip Tags', () => {
wrapper = mountWithContexts(
<PromptJobTemplateDetail
resource={{
...mockJT,
skip_tags: '',
}}
/>
);
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0);
});
}); });

View File

@@ -57,9 +57,11 @@ function PromptWFJobTemplateDetail({ resource }) {
return ( return (
<> <>
{summary_fields?.recent_jobs?.length > 0 && ( <Detail
<Detail value={<Sparkline jobs={recentJobs} />} label={t`Activity`} /> label={t`Activity`}
)} value={<Sparkline jobs={recentJobs} />}
isEmpty={summary_fields?.recent_jobs?.length === 0}
/>
{summary_fields?.organization && ( {summary_fields?.organization && (
<Detail <Detail
label={t`Organization`} label={t`Organization`}
@@ -108,7 +110,7 @@ function PromptWFJobTemplateDetail({ resource }) {
} }
/> />
)} )}
{summary_fields?.labels?.results?.length > 0 && ( {summary_fields?.labels?.results && (
<Detail <Detail
fullWidth fullWidth
label={t`Labels`} label={t`Labels`}
@@ -125,6 +127,7 @@ function PromptWFJobTemplateDetail({ resource }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={summary_fields?.labels?.results?.length === 0}
/> />
)} )}
{extra_vars && ( {extra_vars && (

View File

@@ -62,4 +62,36 @@ describe('PromptWFJobTemplateDetail', () => {
'---\nmock: data' '---\nmock: data'
); );
}); });
test('should not load Activity', () => {
wrapper = mountWithContexts(
<PromptWFJobTemplateDetail
resource={{
...mockWF,
summary_fields: {
recent_jobs: [],
},
}}
/>
);
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Labels', () => {
wrapper = mountWithContexts(
<PromptWFJobTemplateDetail
resource={{
...mockWF,
summary_fields: {
labels: {
results: [],
},
},
}}
/>
);
const labels_detail = wrapper.find(`Detail[label="Labels"]`).at(0);
expect(labels_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -68,34 +68,32 @@ function ResourceAccessListItem({ accessRecord, onRoleDelete }) {
<Td dataLabel={t`Last name`}>{accessRecord.last_name}</Td> <Td dataLabel={t`Last name`}>{accessRecord.last_name}</Td>
<Td dataLabel={t`Roles`}> <Td dataLabel={t`Roles`}>
<DetailList stacked> <DetailList stacked>
{userRoles.length > 0 && ( <Detail
<Detail label={t`User Roles`}
label={t`User Roles`} value={
value={ <ChipGroup
<ChipGroup numChips={5}
numChips={5} totalChips={userRoles.length}
totalChips={userRoles.length} ouiaId="user-role-chips"
ouiaId="user-role-chips" >
> {userRoles.map(renderChip)}
{userRoles.map(renderChip)} </ChipGroup>
</ChipGroup> }
} isEmpty={userRoles.length === 0}
/> />
)} <Detail
{teamRoles.length > 0 && ( label={t`Team Roles`}
<Detail value={
label={t`Team Roles`} <ChipGroup
value={ numChips={5}
<ChipGroup totalChips={teamRoles.length}
numChips={5} ouiaId="team-role-chips"
totalChips={teamRoles.length} >
ouiaId="team-role-chips" {teamRoles.map(renderChip)}
> </ChipGroup>
{teamRoles.map(renderChip)} }
</ChipGroup> isEmpty={teamRoles.length === 0}
} />
/>
)}
</DetailList> </DetailList>
</Td> </Td>
</Tr> </Tr>

View File

@@ -53,5 +53,41 @@ describe('<ResourceAccessListItem />', () => {
expect(wrapper.find('Td[dataLabel="First name"]').text()).toBe('jane'); expect(wrapper.find('Td[dataLabel="First name"]').text()).toBe('jane');
expect(wrapper.find('Td[dataLabel="Last name"]').text()).toBe('brown'); expect(wrapper.find('Td[dataLabel="Last name"]').text()).toBe('brown');
const user_roles_detail = wrapper.find(`Detail[label="User Roles"]`).at(0);
expect(user_roles_detail.prop('isEmpty')).toEqual(true);
});
test('should not load team roles', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<table>
<tbody>
<ResourceAccessListItem
accessRecord={{
...accessRecord,
summary_fields: {
direct_access: [
{
role: {
id: 3,
name: 'Member',
user_capabilities: { unattach: true },
},
},
],
indirect_access: [],
},
}}
onRoleDelete={() => {}}
/>
</tbody>
</table>
);
});
const team_roles_detail = wrapper.find(`Detail[label="Team Roles"]`).at(0);
expect(team_roles_detail.prop('isEmpty')).toEqual(true);
}); });
}); });

View File

@@ -272,11 +272,12 @@ function TemplateListItem({
value={template.description} value={template.description}
dataCy={`template-${template.id}-description`} dataCy={`template-${template.id}-description`}
/> />
{summaryFields.recent_jobs && summaryFields.recent_jobs.length ? ( {summaryFields.recent_jobs ? (
<Detail <Detail
label={t`Activity`} label={t`Activity`}
value={<Sparkline jobs={summaryFields.recent_jobs} />} value={<Sparkline jobs={summaryFields.recent_jobs} />}
dataCy={`template-${template.id}-activity`} dataCy={`template-${template.id}-activity`}
isEmpty={summaryFields.recent_jobs.length === 0}
/> />
) : null} ) : null}
{summaryFields.inventory ? ( {summaryFields.inventory ? (
@@ -316,7 +317,7 @@ function TemplateListItem({
value={formatDateString(template.modified)} value={formatDateString(template.modified)}
dataCy={`template-${template.id}-last-modified`} dataCy={`template-${template.id}-last-modified`}
/> />
{summaryFields.credentials && summaryFields.credentials.length ? ( {summaryFields.credentials ? (
<Detail <Detail
fullWidth fullWidth
label={t`Credentials`} label={t`Credentials`}
@@ -337,9 +338,10 @@ function TemplateListItem({
</ChipGroup> </ChipGroup>
} }
dataCy={`template-${template.id}-credentials`} dataCy={`template-${template.id}-credentials`}
isEmpty={summaryFields.credentials.length === 0}
/> />
) : null} ) : null}
{summaryFields.labels && summaryFields.labels.results.length > 0 && ( {summaryFields.labels && (
<Detail <Detail
fullWidth fullWidth
label={t`Labels`} label={t`Labels`}
@@ -361,6 +363,7 @@ function TemplateListItem({
</ChipGroup> </ChipGroup>
} }
dataCy={`template-${template.id}-labels`} dataCy={`template-${template.id}-labels`}
isEmpty={summaryFields.labels.results.length === 0}
/> />
)} )}
</DetailList> </DetailList>

View File

@@ -465,4 +465,68 @@ describe('<TemplateListItem />', () => {
).toEqual(true); ).toEqual(true);
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(1); expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(1);
}); });
test('should not load Activity', async () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<TemplateListItem
template={{
...mockJobTemplateData,
summary_fields: {
user_capabilities: {},
recent_jobs: [],
},
}}
/>
</tbody>
</table>
);
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Credentials', async () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<TemplateListItem
template={{
...mockJobTemplateData,
summary_fields: {
user_capabilities: {},
credentials: [],
},
}}
/>
</tbody>
</table>
);
const credentials_detail = wrapper
.find(`Detail[label="Credentials"]`)
.at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Labels', async () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<TemplateListItem
template={{
...mockJobTemplateData,
summary_fields: {
user_capabilities: {},
labels: {
results: [],
},
},
}}
/>
</tbody>
</table>
);
const labels_detail = wrapper.find(`Detail[label="Labels"]`).at(0);
expect(labels_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -264,20 +264,19 @@ function CredentialDetail({ credential }) {
date={modified} date={modified}
user={modified_by} user={modified_by}
/> />
{enabledBooleanFields.length > 0 && ( <Detail
<Detail label={t`Enabled Options`}
label={t`Enabled Options`} value={
value={ <TextList component={TextListVariants.ul}>
<TextList component={TextListVariants.ul}> {enabledBooleanFields.map(({ id, label }) => (
{enabledBooleanFields.map(({ id, label }) => ( <TextListItem key={id} component={TextListItemVariants.li}>
<TextListItem key={id} component={TextListItemVariants.li}> {label}
{label} </TextListItem>
</TextListItem> ))}
))} </TextList>
</TextList> }
} isEmpty={enabledBooleanFields.length === 0}
/> />
)}
</DetailList> </DetailList>
{Object.keys(inputSources).length > 0 && ( {Object.keys(inputSources).length > 0 && (
<PluginFieldText> <PluginFieldText>

View File

@@ -149,4 +149,23 @@ describe('<CredentialDetail />', () => {
wrapper.find('ModalBoxCloseButton').invoke('onClose')(); wrapper.find('ModalBoxCloseButton').invoke('onClose')();
}); });
}); });
test('should not load enabled options', async () => {
await act(async () => {
wrapper = mountWithContexts(
<CredentialDetail
credential={{
...mockCredential,
results: {
inputs: null,
},
}}
/>
);
});
const enabled_options_detail = wrapper
.find(`Detail[label="Enabled Options"]`)
.at(0);
expect(enabled_options_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -67,9 +67,11 @@ function HostDetail({ host }) {
<HostToggle host={host} css="padding-bottom: 40px" /> <HostToggle host={host} css="padding-bottom: 40px" />
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={t`Name`} value={name} dataCy="host-name" /> <Detail label={t`Name`} value={name} dataCy="host-name" />
{recentJobs?.length > 0 && ( <Detail
<Detail label={t`Activity`} value={<Sparkline jobs={recentJobs} />} /> label={t`Activity`}
)} value={<Sparkline jobs={recentJobs} />}
isEmpty={recentJobs?.length === 0}
/>
<Detail label={t`Description`} value={description} /> <Detail label={t`Description`} value={description} />
<Detail <Detail
label={t`Inventory`} label={t`Inventory`}

View File

@@ -81,6 +81,8 @@ describe('<HostDetail />', () => {
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength( expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(
0 0
); );
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
}); });
test('should hide edit button for users without edit permission', async () => { test('should hide edit button for users without edit permission', async () => {

View File

@@ -79,17 +79,17 @@ function InventoryDetail({ inventory }) {
} }
/> />
<Detail label={t`Total hosts`} value={inventory.total_hosts} /> <Detail label={t`Total hosts`} value={inventory.total_hosts} />
{instanceGroups && instanceGroups.length > 0 && ( {instanceGroups && (
<Detail <Detail
fullWidth fullWidth
label={t`Instance Groups`} label={t`Instance Groups`}
value={ value={
<ChipGroup <ChipGroup
numChips={5} numChips={5}
totalChips={instanceGroups.length} totalChips={instanceGroups?.length}
ouiaId="instance-group-chips" ouiaId="instance-group-chips"
> >
{instanceGroups.map((ig) => ( {instanceGroups?.map((ig) => (
<Chip <Chip
key={ig.id} key={ig.id}
isReadOnly isReadOnly
@@ -100,28 +100,29 @@ function InventoryDetail({ inventory }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={instanceGroups.length === 0}
/>
)}
{inventory.summary_fields.labels && (
<Detail
fullWidth
helpText={helpText.labels}
label={t`Labels`}
value={
<ChipGroup
numChips={5}
totalChips={inventory.summary_fields.labels?.results?.length}
>
{inventory.summary_fields.labels?.results?.map((l) => (
<Chip key={l.id} isReadOnly>
{l.name}
</Chip>
))}
</ChipGroup>
}
isEmpty={inventory.summary_fields.labels?.results?.length === 0}
/> />
)} )}
{inventory.summary_fields.labels &&
inventory.summary_fields.labels?.results?.length > 0 && (
<Detail
fullWidth
helpText={helpText.labels}
label={t`Labels`}
value={
<ChipGroup
numChips={5}
totalChips={inventory.summary_fields.labels.results.length}
>
{inventory.summary_fields.labels.results.map((l) => (
<Chip key={l.id} isReadOnly>
{l.name}
</Chip>
))}
</ChipGroup>
}
/>
)}
<VariablesDetail <VariablesDetail
label={t`Variables`} label={t`Variables`}
helpText={helpText.variables()} helpText={helpText.variables()}

View File

@@ -153,6 +153,9 @@ describe('<InventoryDetail />', () => {
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith( expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith(
mockInventory.id mockInventory.id
); );
expect(wrapper.find(`Detail[label="Instance Groups"]`)).toHaveLength(0); const instance_groups_detail = wrapper
.find(`Detail[label="Instance Groups"]`)
.at(0);
expect(instance_groups_detail.prop('isEmpty')).toEqual(true);
}); });
}); });

View File

@@ -72,12 +72,11 @@ function InventoryHostDetail({ host }) {
<HostToggle host={host} css="padding-bottom: 40px" /> <HostToggle host={host} css="padding-bottom: 40px" />
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={t`Name`} value={name} /> <Detail label={t`Name`} value={name} />
{recentPlaybookJobs?.length > 0 && ( <Detail
<Detail label={t`Activity`}
label={t`Activity`} value={<Sparkline jobs={recentPlaybookJobs} />}
value={<Sparkline jobs={recentPlaybookJobs} />} isEmpty={recentPlaybookJobs?.length === 0}
/> />
)}
<Detail label={t`Description`} value={description} /> <Detail label={t`Description`} value={description} />
<UserDateDetail date={created} label={t`Created`} user={created_by} /> <UserDateDetail date={created} label={t`Created`} user={created_by} />
<UserDateDetail <UserDateDetail

View File

@@ -91,6 +91,8 @@ describe('<InventoryHostDetail />', () => {
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength( expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(
0 0
); );
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
}); });
test('should hide edit button for users without edit permission', async () => { test('should hide edit button for users without edit permission', async () => {

View File

@@ -268,15 +268,14 @@ function InventorySourceDetail({ inventorySource }) {
helpText={helpText.enabledValue} helpText={helpText.enabledValue}
value={enabled_value} value={enabled_value}
/> />
{credentials?.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Credential`}
label={t`Credential`} value={credentials?.map((cred) => (
value={credentials.map((cred) => ( <CredentialChip key={cred?.id} credential={cred} isReadOnly />
<CredentialChip key={cred?.id} credential={cred} isReadOnly /> ))}
))} isEmpty={credentials?.length === 0}
/> />
)}
{optionsList && ( {optionsList && (
<Detail fullWidth label={t`Enabled Options`} value={optionsList} /> <Detail fullWidth label={t`Enabled Options`} value={optionsList} />
)} )}

View File

@@ -237,4 +237,21 @@ describe('InventorySourceDetail', () => {
(el) => el.length === 0 (el) => el.length === 0
); );
}); });
test('should not load Credentials', async () => {
await act(async () => {
wrapper = mountWithContexts(
<InventorySourceDetail
inventorySource={{
...mockInvSource,
summary_fields: {
credentials: [],
},
}}
/>
);
});
const credentials_detail = wrapper.find(`Detail[label="Credential"]`).at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -96,12 +96,11 @@ function SmartInventoryDetail({ inventory }) {
<CardBody> <CardBody>
<DetailList> <DetailList>
<Detail label={t`Name`} value={name} /> <Detail label={t`Name`} value={name} />
{recentJobs.length > 0 && ( <Detail
<Detail label={t`Activity`}
label={t`Activity`} value={<Sparkline jobs={recentJobs} />}
value={<Sparkline jobs={recentJobs} />} isEmpty={recentJobs.length === 0}
/> />
)}
<Detail label={t`Description`} value={description} /> <Detail label={t`Description`} value={description} />
<Detail label={t`Type`} value={t`Smart inventory`} /> <Detail label={t`Type`} value={t`Smart inventory`} />
<Detail <Detail
@@ -118,29 +117,28 @@ function SmartInventoryDetail({ inventory }) {
value={<Label variant="outline">{host_filter}</Label>} value={<Label variant="outline">{host_filter}</Label>}
/> />
<Detail label={t`Total hosts`} value={total_hosts} /> <Detail label={t`Total hosts`} value={total_hosts} />
{instanceGroups.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Instance groups`}
label={t`Instance groups`} value={
value={ <ChipGroup
<ChipGroup numChips={5}
numChips={5} totalChips={instanceGroups.length}
totalChips={instanceGroups.length} ouiaId="instance-group-chips"
ouiaId="instance-group-chips" >
> {instanceGroups.map((ig) => (
{instanceGroups.map((ig) => ( <Chip
<Chip key={ig.id}
key={ig.id} isReadOnly
isReadOnly ouiaId={`instance-group-${ig.id}-chip`}
ouiaId={`instance-group-${ig.id}-chip`} >
> {ig.name}
{ig.name} </Chip>
</Chip> ))}
))} </ChipGroup>
</ChipGroup> }
} isEmpty={instanceGroups.length === 0}
/> />
)}
<VariablesDetail <VariablesDetail
label={t`Variables`} label={t`Variables`}
value={variables} value={variables}

View File

@@ -112,6 +112,41 @@ describe('<SmartInventoryDetail />', () => {
(el) => el.length === 0 (el) => el.length === 0
); );
}); });
test('should not load Activity', async () => {
await act(async () => {
wrapper = mountWithContexts(
<SmartInventoryDetail
inventory={{
...mockSmartInventory,
recent_jobs: [],
}}
/>
);
});
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Instance Groups', async () => {
InventoriesAPI.readInstanceGroups.mockResolvedValue({
data: {
results: [],
},
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<SmartInventoryDetail inventory={mockSmartInventory} />
);
});
wrapper.update();
const instance_groups_detail = wrapper
.find(`Detail[label="Instance groups"]`)
.at(0);
expect(instance_groups_detail.prop('isEmpty')).toEqual(true);
});
}); });
describe('User has read-only permissions', () => { describe('User has read-only permissions', () => {

View File

@@ -28,12 +28,11 @@ function SmartInventoryHostDetail({ host }) {
<CardBody> <CardBody>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={t`Name`} value={name} /> <Detail label={t`Name`} value={name} />
{recentPlaybookJobs?.length > 0 && ( <Detail
<Detail label={t`Activity`}
label={t`Activity`} value={<Sparkline jobs={recentPlaybookJobs} />}
value={<Sparkline jobs={recentPlaybookJobs} />} isEmpty={recentPlaybookJobs?.length === 0}
/> />
)}
<Detail label={t`Description`} value={description} /> <Detail label={t`Description`} value={description} />
<Detail <Detail
label={t`Inventory`} label={t`Inventory`}

View File

@@ -27,4 +27,19 @@ describe('<SmartInventoryHostDetail />', () => {
expect(wrapper.find('Detail[label="Activity"] Sparkline')).toHaveLength(1); expect(wrapper.find('Detail[label="Activity"] Sparkline')).toHaveLength(1);
expect(wrapper.find('VariablesDetail')).toHaveLength(1); expect(wrapper.find('VariablesDetail')).toHaveLength(1);
}); });
test('should not load Activity', () => {
wrapper = mountWithContexts(
<SmartInventoryHostDetail
host={{
...mockHost,
summary_fields: {
recent_jobs: [],
},
}}
/>
);
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -268,15 +268,14 @@ function JobDetail({ job, inventorySourceLabels }) {
</Link> </Link>
} }
/> />
{inventorySourceLabels.length > 0 && ( <Detail
<Detail dataCy="job-inventory-source-type"
dataCy="job-inventory-source-type" label={t`Source`}
label={t`Source`} value={inventorySourceLabels.map(([string, label]) =>
value={inventorySourceLabels.map(([string, label]) => string === job.source ? label : null
string === job.source ? label : null )}
)} isEmpty={inventorySourceLabels.length === 0}
/> />
)}
</> </>
)} )}
{inventory_source && inventory_source.source === 'scm' && ( {inventory_source && inventory_source.source === 'scm' && (
@@ -406,7 +405,7 @@ function JobDetail({ job, inventorySourceLabels }) {
} }
/> />
)} )}
{credentials && credentials.length > 0 && ( {credentials && (
<Detail <Detail
dataCy="job-credentials" dataCy="job-credentials"
fullWidth fullWidth
@@ -428,6 +427,7 @@ function JobDetail({ job, inventorySourceLabels }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={credentials.length === 0}
/> />
)} )}
{labels && labels.count > 0 && ( {labels && labels.count > 0 && (
@@ -451,7 +451,7 @@ function JobDetail({ job, inventorySourceLabels }) {
} }
/> />
)} )}
{job.job_tags && job.job_tags.length > 0 && ( {job.job_tags && (
<Detail <Detail
dataCy="job-tags" dataCy="job-tags"
fullWidth fullWidth
@@ -474,9 +474,10 @@ function JobDetail({ job, inventorySourceLabels }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={job.job_tags.length === 0}
/> />
)} )}
{job.skip_tags && job.skip_tags.length > 0 && ( {job.skip_tags && (
<Detail <Detail
dataCy="job-skip-tags" dataCy="job-skip-tags"
fullWidth fullWidth
@@ -499,6 +500,7 @@ function JobDetail({ job, inventorySourceLabels }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={job.skip_tags.length === 0}
/> />
)} )}
<Detail <Detail

View File

@@ -548,4 +548,64 @@ describe('<JobDetail />', () => {
assertDetail('Inventory', 'Demo Inventory'); assertDetail('Inventory', 'Demo Inventory');
assertDetail('Job Slice Parent', 'True'); assertDetail('Job Slice Parent', 'True');
}); });
test('should not load Source', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
summary_fields: {
inventory_source: {},
user_capabilities: {},
inventory: { id: 1 },
},
}}
inventorySourceLabels={[]}
/>
);
const source_detail = wrapper.find(`Detail[label="Source"]`).at(0);
expect(source_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Credentials', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
summary_fields: {
user_capabilities: {},
credentials: [],
},
}}
/>
);
const credentials_detail = wrapper
.find(`Detail[label="Credentials"]`)
.at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Job Tags', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
job_tags: '',
}}
/>
);
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0);
});
test('should not load Skip Tags', () => {
wrapper = mountWithContexts(
<JobDetail
job={{
...mockJobData,
skip_tags: '',
}}
/>
);
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0);
});
}); });

View File

@@ -30,7 +30,7 @@ function OrganizationDetail({ organization }) {
created, created,
modified, modified,
summary_fields, summary_fields,
galaxy_credentials, galaxy_credentials = [],
} = organization; } = organization;
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(true); const [hasContentLoading, setHasContentLoading] = useState(true);
@@ -121,7 +121,7 @@ function OrganizationDetail({ organization }) {
date={modified} date={modified}
user={summary_fields.modified_by} user={summary_fields.modified_by}
/> />
{instanceGroups && instanceGroups.length > 0 && ( {instanceGroups && (
<Detail <Detail
fullWidth fullWidth
label={t`Instance Groups`} label={t`Instance Groups`}
@@ -145,35 +145,35 @@ function OrganizationDetail({ organization }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={instanceGroups.length === 0}
/> />
)} )}
{galaxy_credentials && galaxy_credentials.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Galaxy Credentials`}
label={t`Galaxy Credentials`} value={
value={ <ChipGroup
<ChipGroup numChips={5}
numChips={5} totalChips={galaxy_credentials?.length}
totalChips={galaxy_credentials.length} ouiaId="galaxy-credential-chips"
ouiaId="galaxy-credential-chips" >
> {galaxy_credentials?.map((credential) => (
{galaxy_credentials.map((credential) => ( <Link
<Link key={credential.id}
to={`/credentials/${credential.id}/details`}
>
<CredentialChip
credential={credential}
key={credential.id} key={credential.id}
to={`/credentials/${credential.id}/details`} isReadOnly
> ouiaId={`galaxy-credential-${credential.id}-chip`}
<CredentialChip />
credential={credential} </Link>
key={credential.id} ))}
isReadOnly </ChipGroup>
ouiaId={`galaxy-credential-${credential.id}-chip`} }
/> isEmpty={galaxy_credentials?.length === 0}
</Link> />
))}
</ChipGroup>
}
/>
)}
</DetailList> </DetailList>
<CardActionsRow> <CardActionsRow>
{summary_fields.user_capabilities.edit && ( {summary_fields.user_capabilities.edit && (

View File

@@ -216,4 +216,44 @@ describe('<OrganizationDetail />', () => {
(el) => el.length === 0 (el) => el.length === 0
); );
}); });
test('should not load instance groups', async () => {
OrganizationsAPI.readInstanceGroups.mockResolvedValue({
data: {
results: [],
},
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<OrganizationDetail organization={mockOrganization} />
);
});
wrapper.update();
const instance_groups_detail = wrapper
.find(`Detail[label="Instance Groups"]`)
.at(0);
expect(instance_groups_detail.prop('isEmpty')).toEqual(true);
});
test('should not load galaxy credentials', async () => {
OrganizationsAPI.readInstanceGroups.mockResolvedValue({ data: {} });
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<OrganizationDetail
organization={{
...mockOrganization,
credential: [],
}}
/>
);
});
wrapper.update();
const galaxy_credentials_detail = wrapper
.find(`Detail[label="Galaxy Credentials"]`)
.at(0);
expect(galaxy_credentials_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -354,7 +354,7 @@ function JobTemplateDetail({ template }) {
helpText={helpText.enabledOptions} helpText={helpText.enabledOptions}
/> />
)} )}
{summary_fields.credentials && summary_fields.credentials.length > 0 && ( {summary_fields.credentials && (
<Detail <Detail
fullWidth fullWidth
label={t`Credentials`} label={t`Credentials`}
@@ -378,9 +378,10 @@ function JobTemplateDetail({ template }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={summary_fields.credentials.length === 0}
/> />
)} )}
{summary_fields.labels && summary_fields.labels.results.length > 0 && ( {summary_fields.labels && (
<Detail <Detail
fullWidth fullWidth
label={t`Labels`} label={t`Labels`}
@@ -399,36 +400,36 @@ function JobTemplateDetail({ template }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={summary_fields.labels.results.length === 0}
/> />
)} )}
{instanceGroups.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Instance Groups`}
label={t`Instance Groups`} dataCy="jt-detail-instance-groups"
dataCy="jt-detail-instance-groups" helpText={helpText.instanceGroups}
helpText={helpText.instanceGroups} value={
value={ <ChipGroup
<ChipGroup numChips={5}
numChips={5} totalChips={instanceGroups.length}
totalChips={instanceGroups.length} ouiaId="instance-group-chips"
ouiaId="instance-group-chips" >
> {instanceGroups.map((ig) => (
{instanceGroups.map((ig) => ( <Link to={`${buildLinkURL(ig)}${ig.id}/details`} key={ig.id}>
<Link to={`${buildLinkURL(ig)}${ig.id}/details`} key={ig.id}> <Chip
<Chip key={ig.id}
key={ig.id} ouiaId={`instance-group-${ig.id}-chip`}
ouiaId={`instance-group-${ig.id}-chip`} isReadOnly
isReadOnly >
> {ig.name}
{ig.name} </Chip>
</Chip> </Link>
</Link> ))}
))} </ChipGroup>
</ChipGroup> }
} isEmpty={instanceGroups.length === 0}
/> />
)} {job_tags && (
{job_tags && job_tags.length > 0 && (
<Detail <Detail
fullWidth fullWidth
label={t`Job Tags`} label={t`Job Tags`}
@@ -451,9 +452,10 @@ function JobTemplateDetail({ template }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={job_tags.length === 0}
/> />
)} )}
{skip_tags && skip_tags.length > 0 && ( {skip_tags && (
<Detail <Detail
fullWidth fullWidth
label={t`Skip Tags`} label={t`Skip Tags`}
@@ -476,6 +478,7 @@ function JobTemplateDetail({ template }) {
))} ))}
</ChipGroup> </ChipGroup>
} }
isEmpty={skip_tags.length === 0}
/> />
)} )}
<VariablesDetail <VariablesDetail

View File

@@ -195,4 +195,94 @@ describe('<JobTemplateDetail />', () => {
wrapper.find(`Detail[label="Execution Environment"] dd`).text() wrapper.find(`Detail[label="Execution Environment"] dd`).text()
).toBe('Default EE'); ).toBe('Default EE');
}); });
test('should not load credentials', async () => {
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail
template={{
...mockTemplate,
allow_simultaneous: true,
ask_inventory_on_launch: true,
summary_fields: {
credentials: [],
},
}}
/>
);
});
const credentials_detail = wrapper
.find(`Detail[label="Credentials"]`)
.at(0);
expect(credentials_detail.prop('isEmpty')).toEqual(true);
});
test('should not load labels', async () => {
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail
template={{
...mockTemplate,
allow_simultaneous: true,
ask_inventory_on_launch: true,
summary_fields: {
labels: {
results: [],
},
},
}}
/>
);
});
const labels_detail = wrapper.find(`Detail[label="Labels"]`).at(0);
expect(labels_detail.prop('isEmpty')).toEqual(true);
});
test('should not load instance groups', async () => {
JobTemplatesAPI.readInstanceGroups.mockResolvedValue({
data: {
results: [],
},
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail template={mockTemplate} />
);
});
wrapper.update();
const instance_groups_detail = wrapper
.find(`Detail[label="Instance Groups"]`)
.at(0);
expect(instance_groups_detail.prop('isEmpty')).toEqual(true);
});
test('should not load job tags', async () => {
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail
template={{
...mockTemplate,
job_tags: '',
}}
/>
);
});
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0);
});
test('should not load skip tags', async () => {
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail
template={{
...mockTemplate,
skip_tags: '',
}}
/>
);
});
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0);
});
}); });

View File

@@ -110,12 +110,11 @@ function WorkflowJobTemplateDetail({ template }) {
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={t`Name`} value={name} dataCy="jt-detail-name" /> <Detail label={t`Name`} value={name} dataCy="jt-detail-name" />
<Detail label={t`Description`} value={description} /> <Detail label={t`Description`} value={description} />
{summary_fields.recent_jobs?.length > 0 && ( <Detail
<Detail value={<Sparkline jobs={recentPlaybookJobs} />}
value={<Sparkline jobs={recentPlaybookJobs} />} label={t`Activity`}
label={t`Activity`} isEmpty={summary_fields.recent_jobs?.length === 0}
/> />
)}
{summary_fields.organization && ( {summary_fields.organization && (
<Detail <Detail
label={t`Organization`} label={t`Organization`}
@@ -202,26 +201,25 @@ function WorkflowJobTemplateDetail({ template }) {
helpText={helpText.enabledOptions} helpText={helpText.enabledOptions}
/> />
)} )}
{summary_fields.labels?.results?.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Labels`}
label={t`Labels`} helpText={helpText.labels}
helpText={helpText.labels} value={
value={ <ChipGroup
<ChipGroup numChips={3}
numChips={3} totalChips={summary_fields.labels.results.length}
totalChips={summary_fields.labels.results.length} ouiaId="workflow-job-template-detail-label-chips"
ouiaId="workflow-job-template-detail-label-chips" >
> {summary_fields.labels.results.map((l) => (
{summary_fields.labels.results.map((l) => ( <Chip key={l.id} ouiaId={`${l.name}-label-chip`} isReadOnly>
<Chip key={l.id} ouiaId={`${l.name}-label-chip`} isReadOnly> {l.name}
{l.name} </Chip>
</Chip> ))}
))} </ChipGroup>
</ChipGroup> }
} isEmpty={!summary_fields.labels?.results?.length}
/> />
)}
<VariablesDetail <VariablesDetail
dataCy="workflow-job-template-detail-extra-vars" dataCy="workflow-job-template-detail-extra-vars"
helpText={helpText.variables} helpText={helpText.variables}

View File

@@ -178,4 +178,46 @@ describe('<WorkflowJobTemplateDetail/>', () => {
expect(inventory.prop('to')).toEqual('/inventories/inventory/1/details'); expect(inventory.prop('to')).toEqual('/inventories/inventory/1/details');
expect(organization.prop('to')).toEqual('/organizations/1/details'); expect(organization.prop('to')).toEqual('/organizations/1/details');
}); });
test('should not load Activity', async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplateDetail
template={{
...template,
summary_fields: {
...template.summary_fields,
recent_jobs: [],
},
}}
hasContentLoading={false}
onSetContentLoading={() => {}}
/>
);
});
const activity_detail = wrapper.find(`Detail[label="Activity"]`).at(0);
expect(activity_detail.prop('isEmpty')).toEqual(true);
});
test('should not load Labels', async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplateDetail
template={{
...template,
summary_fields: {
...template.summary_fields,
labels: {
results: [],
},
},
}}
hasContentLoading={false}
onSetContentLoading={() => {}}
/>
);
});
const labels_detail = wrapper.find(`Detail[label="Labels"]`).at(0);
expect(labels_detail.prop('isEmpty')).toEqual(true);
});
}); });

View File

@@ -44,30 +44,28 @@ function TemplatePopoverContent({ template }) {
value={template?.playbook} value={template?.playbook}
dataCy={`template-${template.id}-playbook`} dataCy={`template-${template.id}-playbook`}
/> />
{template.summary_fields?.credentials && <Detail
template.summary_fields.credentials.length ? ( fullWidth
<Detail label={t`Credentials`}
fullWidth dataCy={`template-${template.id}-credentials`}
label={t`Credentials`} value={
dataCy={`template-${template.id}-credentials`} <ChipGroup
value={ numChips={5}
<ChipGroup totalChips={template.summary_fields?.credentials?.length}
numChips={5} ouiaId={`template-${template.id}-credential-chips`}
totalChips={template.summary_fields.credentials.length} >
ouiaId={`template-${template.id}-credential-chips`} {template.summary_fields?.credentials?.map((c) => (
> <CredentialChip
{template.summary_fields.credentials.map((c) => ( key={c.id}
<CredentialChip credential={c}
key={c.id} isReadOnly
credential={c} ouiaId={`credential-${c.id}-chip`}
isReadOnly />
ouiaId={`credential-${c.id}-chip`} ))}
/> </ChipGroup>
))} }
</ChipGroup> isEmpty={template.summary_fields?.credentials?.length === 0}
} />
/>
) : null}
</DetailList> </DetailList>
); );
} }

View File

@@ -309,25 +309,24 @@ function WorkflowApprovalDetail({ workflowApproval }) {
dataCy="wa-detail-inventory" dataCy="wa-detail-inventory"
/> />
) : null} ) : null}
{workflowJob?.summary_fields?.labels?.results?.length > 0 && ( <Detail
<Detail fullWidth
fullWidth label={t`Labels`}
label={t`Labels`} value={
value={ <ChipGroup
<ChipGroup numChips={5}
numChips={5} totalChips={workflowJob.summary_fields.labels.results.length}
totalChips={workflowJob.summary_fields.labels.results.length} ouiaId="wa-detail-label-chips"
ouiaId="wa-detail-label-chips" >
> {workflowJob.summary_fields.labels.results.map((label) => (
{workflowJob.summary_fields.labels.results.map((label) => ( <Chip key={label.id} isReadOnly>
<Chip key={label.id} isReadOnly> {label.name}
{label.name} </Chip>
</Chip> ))}
))} </ChipGroup>
</ChipGroup> }
} isEmpty={!workflowJob?.summary_fields?.labels?.results?.length}
/> />
)}
{workflowJob?.extra_vars ? ( {workflowJob?.extra_vars ? (
<VariablesDetail <VariablesDetail
dataCy="wa-detail-variables" dataCy="wa-detail-variables"

View File

@@ -482,6 +482,33 @@ describe('<WorkflowApprovalDetail />', () => {
expect(wrapper.find('DeleteButton').length).toBe(1); expect(wrapper.find('DeleteButton').length).toBe(1);
}); });
test('should not load Labels', async () => {
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: workflowJobTemplate,
});
WorkflowJobsAPI.readDetail.mockResolvedValue({
data: {
...workflowApproval,
summary_fields: {
...workflowApproval.summary_fields,
labels: {
results: [],
},
},
},
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<WorkflowApprovalDetail workflowApproval={workflowApproval} />
);
});
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
const labels_detail = wrapper.find(`Detail[label="Labels"]`).at(0);
expect(labels_detail.prop('isEmpty')).toEqual(true);
});
test('Error dialog shown for failed approval', async () => { test('Error dialog shown for failed approval', async () => {
WorkflowApprovalsAPI.approve.mockImplementationOnce(() => WorkflowApprovalsAPI.approve.mockImplementationOnce(() =>
Promise.reject(new Error()) Promise.reject(new Error())