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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 954 additions and 313 deletions

View File

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

View File

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

View File

@ -203,6 +203,49 @@ describe('<JobListItem />', () => {
wrapper.find('Detail[label="Execution Environment"] dd').text()
).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 />', () => {

View File

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

View File

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

View File

@ -125,4 +125,92 @@ describe('PromptJobTemplateDetail', () => {
assertDetail(wrapper, 'Organization', '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 (
<>
{summary_fields?.recent_jobs?.length > 0 && (
<Detail value={<Sparkline jobs={recentJobs} />} label={t`Activity`} />
)}
<Detail
label={t`Activity`}
value={<Sparkline jobs={recentJobs} />}
isEmpty={summary_fields?.recent_jobs?.length === 0}
/>
{summary_fields?.organization && (
<Detail
label={t`Organization`}
@ -108,7 +110,7 @@ function PromptWFJobTemplateDetail({ resource }) {
}
/>
)}
{summary_fields?.labels?.results?.length > 0 && (
{summary_fields?.labels?.results && (
<Detail
fullWidth
label={t`Labels`}
@ -125,6 +127,7 @@ function PromptWFJobTemplateDetail({ resource }) {
))}
</ChipGroup>
}
isEmpty={summary_fields?.labels?.results?.length === 0}
/>
)}
{extra_vars && (

View File

@ -62,4 +62,36 @@ describe('PromptWFJobTemplateDetail', () => {
'---\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`Roles`}>
<DetailList stacked>
{userRoles.length > 0 && (
<Detail
label={t`User Roles`}
value={
<ChipGroup
numChips={5}
totalChips={userRoles.length}
ouiaId="user-role-chips"
>
{userRoles.map(renderChip)}
</ChipGroup>
}
/>
)}
{teamRoles.length > 0 && (
<Detail
label={t`Team Roles`}
value={
<ChipGroup
numChips={5}
totalChips={teamRoles.length}
ouiaId="team-role-chips"
>
{teamRoles.map(renderChip)}
</ChipGroup>
}
/>
)}
<Detail
label={t`User Roles`}
value={
<ChipGroup
numChips={5}
totalChips={userRoles.length}
ouiaId="user-role-chips"
>
{userRoles.map(renderChip)}
</ChipGroup>
}
isEmpty={userRoles.length === 0}
/>
<Detail
label={t`Team Roles`}
value={
<ChipGroup
numChips={5}
totalChips={teamRoles.length}
ouiaId="team-role-chips"
>
{teamRoles.map(renderChip)}
</ChipGroup>
}
isEmpty={teamRoles.length === 0}
/>
</DetailList>
</Td>
</Tr>

View File

@ -53,5 +53,41 @@ describe('<ResourceAccessListItem />', () => {
expect(wrapper.find('Td[dataLabel="First name"]').text()).toBe('jane');
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}
dataCy={`template-${template.id}-description`}
/>
{summaryFields.recent_jobs && summaryFields.recent_jobs.length ? (
{summaryFields.recent_jobs ? (
<Detail
label={t`Activity`}
value={<Sparkline jobs={summaryFields.recent_jobs} />}
dataCy={`template-${template.id}-activity`}
isEmpty={summaryFields.recent_jobs.length === 0}
/>
) : null}
{summaryFields.inventory ? (
@ -316,7 +317,7 @@ function TemplateListItem({
value={formatDateString(template.modified)}
dataCy={`template-${template.id}-last-modified`}
/>
{summaryFields.credentials && summaryFields.credentials.length ? (
{summaryFields.credentials ? (
<Detail
fullWidth
label={t`Credentials`}
@ -337,9 +338,10 @@ function TemplateListItem({
</ChipGroup>
}
dataCy={`template-${template.id}-credentials`}
isEmpty={summaryFields.credentials.length === 0}
/>
) : null}
{summaryFields.labels && summaryFields.labels.results.length > 0 && (
{summaryFields.labels && (
<Detail
fullWidth
label={t`Labels`}
@ -361,6 +363,7 @@ function TemplateListItem({
</ChipGroup>
}
dataCy={`template-${template.id}-labels`}
isEmpty={summaryFields.labels.results.length === 0}
/>
)}
</DetailList>

View File

@ -465,4 +465,68 @@ describe('<TemplateListItem />', () => {
).toEqual(true);
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}
user={modified_by}
/>
{enabledBooleanFields.length > 0 && (
<Detail
label={t`Enabled Options`}
value={
<TextList component={TextListVariants.ul}>
{enabledBooleanFields.map(({ id, label }) => (
<TextListItem key={id} component={TextListItemVariants.li}>
{label}
</TextListItem>
))}
</TextList>
}
/>
)}
<Detail
label={t`Enabled Options`}
value={
<TextList component={TextListVariants.ul}>
{enabledBooleanFields.map(({ id, label }) => (
<TextListItem key={id} component={TextListItemVariants.li}>
{label}
</TextListItem>
))}
</TextList>
}
isEmpty={enabledBooleanFields.length === 0}
/>
</DetailList>
{Object.keys(inputSources).length > 0 && (
<PluginFieldText>

View File

@ -149,4 +149,23 @@ describe('<CredentialDetail />', () => {
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" />
<DetailList gutter="sm">
<Detail label={t`Name`} value={name} dataCy="host-name" />
{recentJobs?.length > 0 && (
<Detail label={t`Activity`} value={<Sparkline jobs={recentJobs} />} />
)}
<Detail
label={t`Activity`}
value={<Sparkline jobs={recentJobs} />}
isEmpty={recentJobs?.length === 0}
/>
<Detail label={t`Description`} value={description} />
<Detail
label={t`Inventory`}

View File

@ -81,6 +81,8 @@ describe('<HostDetail />', () => {
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(
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 () => {

View File

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

View File

@ -153,6 +153,9 @@ describe('<InventoryDetail />', () => {
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith(
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" />
<DetailList gutter="sm">
<Detail label={t`Name`} value={name} />
{recentPlaybookJobs?.length > 0 && (
<Detail
label={t`Activity`}
value={<Sparkline jobs={recentPlaybookJobs} />}
/>
)}
<Detail
label={t`Activity`}
value={<Sparkline jobs={recentPlaybookJobs} />}
isEmpty={recentPlaybookJobs?.length === 0}
/>
<Detail label={t`Description`} value={description} />
<UserDateDetail date={created} label={t`Created`} user={created_by} />
<UserDateDetail

View File

@ -91,6 +91,8 @@ describe('<InventoryHostDetail />', () => {
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(
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 () => {

View File

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

View File

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

View File

@ -112,6 +112,41 @@ describe('<SmartInventoryDetail />', () => {
(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', () => {

View File

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

View File

@ -27,4 +27,19 @@ describe('<SmartInventoryHostDetail />', () => {
expect(wrapper.find('Detail[label="Activity"] Sparkline')).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>
}
/>
{inventorySourceLabels.length > 0 && (
<Detail
dataCy="job-inventory-source-type"
label={t`Source`}
value={inventorySourceLabels.map(([string, label]) =>
string === job.source ? label : null
)}
/>
)}
<Detail
dataCy="job-inventory-source-type"
label={t`Source`}
value={inventorySourceLabels.map(([string, label]) =>
string === job.source ? label : null
)}
isEmpty={inventorySourceLabels.length === 0}
/>
</>
)}
{inventory_source && inventory_source.source === 'scm' && (
@ -406,7 +405,7 @@ function JobDetail({ job, inventorySourceLabels }) {
}
/>
)}
{credentials && credentials.length > 0 && (
{credentials && (
<Detail
dataCy="job-credentials"
fullWidth
@ -428,6 +427,7 @@ function JobDetail({ job, inventorySourceLabels }) {
))}
</ChipGroup>
}
isEmpty={credentials.length === 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
dataCy="job-tags"
fullWidth
@ -474,9 +474,10 @@ function JobDetail({ job, inventorySourceLabels }) {
))}
</ChipGroup>
}
isEmpty={job.job_tags.length === 0}
/>
)}
{job.skip_tags && job.skip_tags.length > 0 && (
{job.skip_tags && (
<Detail
dataCy="job-skip-tags"
fullWidth
@ -499,6 +500,7 @@ function JobDetail({ job, inventorySourceLabels }) {
))}
</ChipGroup>
}
isEmpty={job.skip_tags.length === 0}
/>
)}
<Detail

View File

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

View File

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

View File

@ -195,4 +195,94 @@ describe('<JobTemplateDetail />', () => {
wrapper.find(`Detail[label="Execution Environment"] dd`).text()
).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">
<Detail label={t`Name`} value={name} dataCy="jt-detail-name" />
<Detail label={t`Description`} value={description} />
{summary_fields.recent_jobs?.length > 0 && (
<Detail
value={<Sparkline jobs={recentPlaybookJobs} />}
label={t`Activity`}
/>
)}
<Detail
value={<Sparkline jobs={recentPlaybookJobs} />}
label={t`Activity`}
isEmpty={summary_fields.recent_jobs?.length === 0}
/>
{summary_fields.organization && (
<Detail
label={t`Organization`}
@ -202,26 +201,25 @@ function WorkflowJobTemplateDetail({ template }) {
helpText={helpText.enabledOptions}
/>
)}
{summary_fields.labels?.results?.length > 0 && (
<Detail
fullWidth
label={t`Labels`}
helpText={helpText.labels}
value={
<ChipGroup
numChips={3}
totalChips={summary_fields.labels.results.length}
ouiaId="workflow-job-template-detail-label-chips"
>
{summary_fields.labels.results.map((l) => (
<Chip key={l.id} ouiaId={`${l.name}-label-chip`} isReadOnly>
{l.name}
</Chip>
))}
</ChipGroup>
}
/>
)}
<Detail
fullWidth
label={t`Labels`}
helpText={helpText.labels}
value={
<ChipGroup
numChips={3}
totalChips={summary_fields.labels.results.length}
ouiaId="workflow-job-template-detail-label-chips"
>
{summary_fields.labels.results.map((l) => (
<Chip key={l.id} ouiaId={`${l.name}-label-chip`} isReadOnly>
{l.name}
</Chip>
))}
</ChipGroup>
}
isEmpty={!summary_fields.labels?.results?.length}
/>
<VariablesDetail
dataCy="workflow-job-template-detail-extra-vars"
helpText={helpText.variables}

View File

@ -178,4 +178,46 @@ describe('<WorkflowJobTemplateDetail/>', () => {
expect(inventory.prop('to')).toEqual('/inventories/inventory/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}
dataCy={`template-${template.id}-playbook`}
/>
{template.summary_fields?.credentials &&
template.summary_fields.credentials.length ? (
<Detail
fullWidth
label={t`Credentials`}
dataCy={`template-${template.id}-credentials`}
value={
<ChipGroup
numChips={5}
totalChips={template.summary_fields.credentials.length}
ouiaId={`template-${template.id}-credential-chips`}
>
{template.summary_fields.credentials.map((c) => (
<CredentialChip
key={c.id}
credential={c}
isReadOnly
ouiaId={`credential-${c.id}-chip`}
/>
))}
</ChipGroup>
}
/>
) : null}
<Detail
fullWidth
label={t`Credentials`}
dataCy={`template-${template.id}-credentials`}
value={
<ChipGroup
numChips={5}
totalChips={template.summary_fields?.credentials?.length}
ouiaId={`template-${template.id}-credential-chips`}
>
{template.summary_fields?.credentials?.map((c) => (
<CredentialChip
key={c.id}
credential={c}
isReadOnly
ouiaId={`credential-${c.id}-chip`}
/>
))}
</ChipGroup>
}
isEmpty={template.summary_fields?.credentials?.length === 0}
/>
</DetailList>
);
}

View File

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

View File

@ -482,6 +482,33 @@ describe('<WorkflowApprovalDetail />', () => {
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 () => {
WorkflowApprovalsAPI.approve.mockImplementationOnce(() =>
Promise.reject(new Error())