mirror of
https://github.com/ansible/awx.git
synced 2026-03-18 17:37:30 -02:30
Add wf node list item info popover (#11587)
This commit is contained in:
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Td, Tr } from '@patternfly/react-table';
|
import { Td, Tr } from '@patternfly/react-table';
|
||||||
|
import { ActionsTd } from 'components/PaginatedTable';
|
||||||
|
|
||||||
const CheckboxListItem = ({
|
const CheckboxListItem = ({
|
||||||
isRadio = false,
|
isRadio = false,
|
||||||
@@ -15,6 +16,7 @@ const CheckboxListItem = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
columns,
|
columns,
|
||||||
item,
|
item,
|
||||||
|
rowActions,
|
||||||
}) => {
|
}) => {
|
||||||
const handleRowClick = () => {
|
const handleRowClick = () => {
|
||||||
if (isSelected && !isRadio) {
|
if (isSelected && !isRadio) {
|
||||||
@@ -62,6 +64,16 @@ const CheckboxListItem = ({
|
|||||||
<b>{label}</b>
|
<b>{label}</b>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
{rowActions && (
|
||||||
|
<ActionsTd>
|
||||||
|
{rowActions.map((rowAction) => {
|
||||||
|
const {
|
||||||
|
props: { id },
|
||||||
|
} = rowAction;
|
||||||
|
return <React.Fragment key={id}>{rowAction}</React.Fragment>;
|
||||||
|
})}
|
||||||
|
</ActionsTd>
|
||||||
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,4 +21,33 @@ describe('CheckboxListItem', () => {
|
|||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render row actions', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<CheckboxListItem
|
||||||
|
itemId={1}
|
||||||
|
name="Buzz"
|
||||||
|
label="Buzz"
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={() => {}}
|
||||||
|
onDeselect={() => {}}
|
||||||
|
rowActions={[
|
||||||
|
<div id="1">action_1</div>,
|
||||||
|
<div id="2">action_2</div>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('ActionsTd')
|
||||||
|
.containsAllMatchingElements([
|
||||||
|
<div id="1">action_1</div>,
|
||||||
|
<div id="2">action_2</div>,
|
||||||
|
])
|
||||||
|
).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { TextList, TextListVariants } from '@patternfly/react-core';
|
import { TextList, TextListVariants } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const DetailList = ({ children, stacked, ...props }) => (
|
const DetailList = ({ children, stacked, compact, ...props }) => (
|
||||||
<TextList component={TextListVariants.dl} {...props}>
|
<TextList component={TextListVariants.dl} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</TextList>
|
</TextList>
|
||||||
@@ -10,8 +10,8 @@ const DetailList = ({ children, stacked, ...props }) => (
|
|||||||
|
|
||||||
export default styled(DetailList)`
|
export default styled(DetailList)`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 20px;
|
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
${(props) => (props.compact ? `column-gap: 20px;` : `grid-gap: 20px;`)}
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.stacked
|
props.stacked
|
||||||
? `
|
? `
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ import React, { useEffect, useCallback } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { Popover } from '@patternfly/react-core';
|
||||||
|
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
import { func, shape } from 'prop-types';
|
import { func, shape } from 'prop-types';
|
||||||
import { JobTemplatesAPI } from 'api';
|
import { JobTemplatesAPI } from 'api';
|
||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import useRequest from 'hooks/useRequest';
|
import useRequest from 'hooks/useRequest';
|
||||||
import DataListToolbar from 'components/DataListToolbar';
|
|
||||||
import CheckboxListItem from 'components/CheckboxListItem';
|
import CheckboxListItem from 'components/CheckboxListItem';
|
||||||
|
import ChipGroup from 'components/ChipGroup';
|
||||||
|
import CredentialChip from 'components/CredentialChip';
|
||||||
|
import DataListToolbar from 'components/DataListToolbar';
|
||||||
|
import { Detail, DetailList } from 'components/DetailList';
|
||||||
import PaginatedTable, {
|
import PaginatedTable, {
|
||||||
|
ActionItem,
|
||||||
HeaderCell,
|
HeaderCell,
|
||||||
HeaderRow,
|
HeaderRow,
|
||||||
getSearchableKeys,
|
getSearchableKeys,
|
||||||
@@ -20,6 +26,52 @@ const QS_CONFIG = getQSConfig('job-templates', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function TemplatePopoverContent({ template }) {
|
||||||
|
return (
|
||||||
|
<DetailList compact stacked>
|
||||||
|
<Detail
|
||||||
|
label={t`Inventory`}
|
||||||
|
value={template.summary_fields?.inventory?.name}
|
||||||
|
dataCy={`template-${template.id}-inventory`}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Project`}
|
||||||
|
value={template.summary_fields?.project?.name}
|
||||||
|
dataCy={`template-${template.id}-project`}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Playbook`}
|
||||||
|
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}
|
||||||
|
</DetailList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function JobTemplatesList({ nodeResource, onUpdateNodeResource }) {
|
function JobTemplatesList({ nodeResource, onUpdateNodeResource }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -81,6 +133,18 @@ function JobTemplatesList({ nodeResource, onUpdateNodeResource }) {
|
|||||||
onSelect={() => onUpdateNodeResource(item)}
|
onSelect={() => onUpdateNodeResource(item)}
|
||||||
onDeselect={() => onUpdateNodeResource(null)}
|
onDeselect={() => onUpdateNodeResource(null)}
|
||||||
isRadio
|
isRadio
|
||||||
|
rowActions={[
|
||||||
|
<ActionItem id={item.id} visible>
|
||||||
|
<Popover
|
||||||
|
bodyContent={<TemplatePopoverContent template={item} />}
|
||||||
|
headerContent={<div>{t`Details`}</div>}
|
||||||
|
id={`item-${item.id}-info-popover`}
|
||||||
|
position="right"
|
||||||
|
>
|
||||||
|
<OutlinedQuestionCircleIcon />
|
||||||
|
</Popover>
|
||||||
|
</ActionItem>,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderToolbar={(props) => <DataListToolbar {...props} fillWidth />}
|
renderToolbar={(props) => <DataListToolbar {...props} fillWidth />}
|
||||||
|
|||||||
@@ -82,6 +82,45 @@ describe('JobTemplatesList', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Row should display popover', async () => {
|
||||||
|
JobTemplatesAPI.read.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
count: 1,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Test Job Template',
|
||||||
|
type: 'job_template',
|
||||||
|
url: '/api/v2/job_templates/1',
|
||||||
|
inventory: 1,
|
||||||
|
project: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<JobTemplatesList
|
||||||
|
nodeResource={nodeResource}
|
||||||
|
onUpdateNodeResource={onUpdateNodeResource}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper.find('CheckboxListItem[name="Test Job Template"] Popover').length
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('Error shown when read() request errors', async () => {
|
test('Error shown when read() request errors', async () => {
|
||||||
JobTemplatesAPI.read.mockRejectedValue(new Error());
|
JobTemplatesAPI.read.mockRejectedValue(new Error());
|
||||||
JobTemplatesAPI.readOptions.mockResolvedValue({
|
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||||
|
|||||||
Reference in New Issue
Block a user