Adds expand all to Host List, and moved Survey list to table view

This commit is contained in:
Alex Corey 2021-08-02 14:47:29 -04:00
parent 80053cea83
commit 0aa82c2784
6 changed files with 257 additions and 185 deletions

View File

@ -14,6 +14,7 @@ import PaginatedTable, {
} from 'components/PaginatedTable';
import useRequest, { useDeleteItems } from 'hooks/useRequest';
import useSelected from 'hooks/useSelected';
import useExpanded from 'hooks/useExpanded';
import { encodeQueryString, getQSConfig, parseQueryString } from 'util/qs';
import HostListItem from './HostListItem';
@ -88,6 +89,9 @@ function HostList() {
const { selected, isAllSelected, handleSelect, selectAll, clearSelected } =
useSelected(hosts);
const { expanded, isAllExpanded, handleExpand, expandAll } =
useExpanded(hosts);
const {
isLoading: isDeleteLoading,
deleteItems: deleteHosts,
@ -165,6 +169,8 @@ function HostList() {
{...props}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
isAllExpanded={isAllExpanded}
onExpandAll={expandAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
@ -195,6 +201,8 @@ function HostList() {
<HostListItem
key={host.id}
host={host}
isExpanded={expanded.some((row) => row.id === host.id)}
onExpand={() => handleExpand(host)}
detailUrl={`${match.url}/${host.id}/details`}
isSelected={selected.some((row) => row.id === host.id)}
onSelect={() => handleSelect(host)}

View File

@ -1,5 +1,5 @@
import 'styled-components/macro';
import React, { useState } from 'react';
import React from 'react';
import { string, bool, func } from 'prop-types';
import { t } from '@lingui/macro';
@ -13,9 +13,16 @@ import HostToggle from 'components/HostToggle';
import { DetailList, Detail } from 'components/DetailList';
import Sparkline from 'components/Sparkline';
function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
function HostListItem({
host,
isSelected,
onSelect,
detailUrl,
rowIndex,
isExpanded,
onExpand,
}) {
const labelId = `check-action-${host.id}`;
const [isExpanded, setIsExpanded] = useState(false);
const {
summary_fields: { recent_jobs: recentJobs = [] },
@ -28,7 +35,7 @@ function HostListItem({ host, isSelected, onSelect, detailUrl, rowIndex }) {
expand={{
rowIndex,
isExpanded,
onToggle: () => setIsExpanded(!isExpanded),
onToggle: onExpand,
}}
/>
<Td

View File

@ -3,13 +3,13 @@ import React, { useState } from 'react';
import { t } from '@lingui/macro';
import { useRouteMatch } from 'react-router-dom';
import {
DataList,
Button as _Button,
Title,
EmptyState,
EmptyStateIcon,
EmptyStateBody,
} from '@patternfly/react-core';
import { TableComposable, Thead, Tr, Th, Tbody } from '@patternfly/react-table';
import { CubesIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import ContentLoading from 'components/ContentLoading';
@ -23,6 +23,7 @@ import SurveyPreviewModal from './SurveyPreviewModal';
const Button = styled(_Button)`
margin: 20px;
max-width: 85px;
`;
function SurveyList({
@ -129,20 +130,43 @@ function SurveyList({
content = <ContentLoading />;
} else {
content = (
<DataList aria-label={t`Survey List`}>
{questions?.map((question, index) => (
<SurveyListItem
key={question.variable}
isLast={index === questions.length - 1}
isFirst={index === 0}
question={question}
isChecked={selected.some((q) => q.variable === question.variable)}
onSelect={() => handleSelect(question)}
onMoveUp={moveUp}
onMoveDown={moveDown}
canEdit={canEdit}
/>
))}
<>
<TableComposable>
<Thead>
<Tr>
<Th />
<Th>{t`Name`}</Th>
<Th>{t`Type`}</Th>
<Th>{t`Default`}</Th>
<Th>{t`Actions`}</Th>
</Tr>
</Thead>
<Tbody>
{questions?.map((question, index) => (
<SurveyListItem
key={question.variable}
isLast={index === questions.length - 1}
isFirst={index === 0}
question={question}
isChecked={selected.some(
(q) => q.variable === question.variable
)}
onSelect={() => handleSelect(question)}
onMoveUp={moveUp}
onMoveDown={moveDown}
canEdit={canEdit}
rowIndex={index}
/>
))}
</Tbody>
</TableComposable>
<Button
onClick={() => setIsPreviewModalOpen(true)}
variant="primary"
aria-label={t`Preview`}
>
{t`Preview`}
</Button>
{isDeleteModalOpen && deleteModal}
{isPreviewModalOpen && (
<SurveyPreviewModal
@ -151,14 +175,7 @@ function SurveyList({
questions={questions}
/>
)}
<Button
onClick={() => setIsPreviewModalOpen(true)}
variant="primary"
aria-label={t`Preview`}
>
{t`Preview`}
</Button>
</DataList>
</>
);
}

View File

@ -5,16 +5,11 @@ import { Link } from 'react-router-dom';
import {
Button as _Button,
Chip,
DataListAction as _DataListAction,
DataListCell,
DataListCheck,
DataListItemCells,
DataListItemRow,
DataListItem,
Stack,
StackItem,
Tooltip,
} from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import {
CaretDownIcon,
CaretUpIcon,
@ -22,20 +17,12 @@ import {
} from '@patternfly/react-icons';
import styled from 'styled-components';
import ChipGroup from 'components/ChipGroup';
import { ActionItem, ActionsTd } from 'components/PaginatedTable';
const DataListAction = styled(_DataListAction)`
&& {
margin-left: 0;
margin-right: 20px;
padding-top: 0;
padding-bottom: 0;
}
`;
const Button = styled(_Button)`
const StackButton = styled(_Button)`
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
padding-left: 20px;
`;
const Required = styled.span`
@ -43,12 +30,6 @@ const Required = styled.span`
margin-left: var(--pf-global--spacer--xs);
`;
const Label = styled.b`
margin-right: 20px;
`;
const EditSection = styled(_DataListAction)``;
const EditButton = styled(_Button)``;
function SurveyListItem({
@ -60,121 +41,109 @@ function SurveyListItem({
onSelect,
onMoveUp,
onMoveDown,
rowIndex,
}) {
return (
<DataListItem
aria-labelledby={t`Survey questions`}
id={`survey-list-item-${question.variable}`}
>
<DataListItemRow css="padding-left:16px">
<DataListAction
id="sortQuestions"
aria-labelledby={t`Sort question order`}
aria-label={t`Sort question order`}
>
<Stack>
<StackItem>
<Button
ouiaId={`${question.variable}-move-up-button`}
variant="plain"
aria-label={t`move up`}
isDisabled={isFirst || !canEdit}
onClick={() => onMoveUp(question)}
>
<CaretUpIcon />
</Button>
</StackItem>
<StackItem>
<Button
ouiaId={`${question.variable}-move-down-button`}
variant="plain"
aria-label={t`move down`}
isDisabled={isLast || !canEdit}
onClick={() => onMoveDown(question)}
>
<CaretDownIcon />
</Button>
</StackItem>
</Stack>
</DataListAction>
<DataListCheck
isDisabled={!canEdit}
checked={isChecked}
onChange={onSelect}
aria-labelledby="survey check"
/>
<DataListItemCells
dataListCells={[
<DataListCell key="name">
<>
<Link
to={`survey/edit?question_variable=${encodeURIComponent(
question.variable
)}`}
>
{question.question_name}
</Link>
{question.required && (
<Required
aria-label={t`Required`}
className="pf-c-form__label-required"
aria-hidden="true"
>
*
</Required>
)}
</>
</DataListCell>,
<DataListCell key="type">
<Label>{t`Type`}</Label>
{question.type}
</DataListCell>,
<DataListCell key="default">
<Label>{t`Default`}</Label>
{[question.type].includes('password') && (
<span>{t`encrypted`.toUpperCase()}</span>
)}
{[question.type].includes('multiselect') &&
question.default.length > 0 && (
<ChipGroup
numChips={5}
totalChips={question.default.split('\n').length}
>
{question.default.split('\n').map((chip) => (
<Chip key={chip} isReadOnly>
{chip}
</Chip>
))}
</ChipGroup>
)}
{![question.type].includes('password') &&
![question.type].includes('multiselect') && (
<span>{question.default}</span>
)}
</DataListCell>,
]}
/>
<EditSection aria-label={t`actions`}>
{canEdit && (
<EditButton variant="plain">
<Tooltip content={t`Edit Survey`} position="top">
<EditButton
ouiaId={`edit-survey-${question.variable}`}
aria-label={t`edit survey`}
variant="plain"
component={Link}
to={`survey/edit?question_variable=${encodeURIComponent(
question.variable
)}`}
>
<PencilAltIcon />
</EditButton>
</Tooltip>
</EditButton>
<Tr>
<Td
select={{
rowIndex,
isSelected: isChecked,
onSelect,
}}
dataLabel={t`Selected`}
/>
<Td id={`survey-list-item-${question.variable}`} dataLabel={t`Name`}>
<>
<Link
to={`survey/edit?question_variable=${encodeURIComponent(
question.variable
)}`}
>
{question.question_name}
</Link>
{question.required && (
<Required
aria-label={t`Required`}
className="pf-c-form__label-required"
aria-hidden="true"
>
*
</Required>
)}
</EditSection>
</DataListItemRow>
</DataListItem>
</>
</Td>
<Td dataLabel={t`Type`}>{question.type}</Td>
<Td dataLabel={t`Default`}>
{[question.type].includes('password') && (
<span>{t`encrypted`.toUpperCase()}</span>
)}
{[question.type].includes('multiselect') &&
question.default.length > 0 && (
<ChipGroup
numChips={5}
totalChips={question.default.split('\n').length}
>
{question.default.split('\n').map((chip) => (
<Chip key={chip} isReadOnly>
{chip}
</Chip>
))}
</ChipGroup>
)}
{![question.type].includes('password') &&
![question.type].includes('multiselect') && (
<span>{question.default}</span>
)}
</Td>
<ActionsTd dataLabel={t`Actions`}>
<ActionItem visible={canEdit}>
<EditButton variant="plain">
<Tooltip content={t`Edit Survey`} position="top">
<EditButton
ouiaId={`edit-survey-${question.variable}`}
aria-label={t`edit survey`}
variant="plain"
component={Link}
to={`survey/edit?question_variable=${encodeURIComponent(
question.variable
)}`}
>
<PencilAltIcon />
</EditButton>
</Tooltip>
</EditButton>
</ActionItem>
<ActionItem visible={canEdit}>
<>
<Stack>
<StackItem>
<StackButton
ouiaId={`${question.variable}-move-up-button`}
variant="plain"
aria-label={t`move up`}
isDisabled={isFirst || !canEdit}
onClick={() => onMoveUp(question)}
>
<CaretUpIcon />
</StackButton>
</StackItem>
<StackItem>
<StackButton
ouiaId={`${question.variable}-move-down-button`}
variant="plain"
aria-label={t`move down`}
isDisabled={isLast || !canEdit}
onClick={() => onMoveDown(question)}
>
<CaretDownIcon />
</StackButton>
</StackItem>
</Stack>
</>
</ActionItem>
</ActionsTd>
</Tr>
);
}
export default SurveyListItem;

View File

@ -1,6 +1,9 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import {
mountWithContexts,
shallowWithContexts,
} from '../../../../testUtils/enzymeHelpers';
import SurveyListItem from './SurveyListItem';
describe('<SurveyListItem />', () => {
@ -11,36 +14,57 @@ describe('<SurveyListItem />', () => {
type: 'text',
id: 1,
};
test('renders successfully', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
wrapper = shallowWithContexts(
<SurveyListItem question={item} isFirst={false} isLast={false} />
);
});
expect(wrapper.length).toBe(1);
});
test('fields are rendering properly', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={item} isFirst={false} isLast={false} />
<table>
<tbody>
<SurveyListItem
question={item}
isFirst={false}
isLast={false}
canEdit
/>
</tbody>
</table>
);
});
const moveUp = wrapper.find('Button[aria-label="move up"]');
const moveDown = wrapper.find('Button[aria-label="move down"]');
expect(moveUp.length).toBe(1);
expect(moveDown.length).toBe(1);
expect(wrapper.find('b').at(0).text()).toBe('Type');
expect(wrapper.find('b').at(1).text()).toBe('Default');
expect(wrapper.find('DataListCheck').length).toBe(1);
expect(wrapper.find('DataListCell').length).toBe(3);
expect(wrapper.find('SelectColumn').length).toBe(1);
expect(wrapper.find('Td').length).toBe(5);
});
test('move up and move down buttons are disabled', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={item} isChecked={false} isFirst isLast />
<table>
<tbody>
<SurveyListItem
rowIndex={1}
question={item}
isChecked={false}
isFirst
isLast
canEdit
/>
</tbody>
</table>
);
});
const moveUp = wrapper
@ -49,9 +73,11 @@ describe('<SurveyListItem />', () => {
const moveDown = wrapper
.find('Button[aria-label="move down"]')
.prop('isDisabled');
expect(moveUp).toBe(true);
expect(moveDown).toBe(true);
});
test('required item has required asterisk', () => {
const newItem = {
question_name: 'Foo',
@ -64,7 +90,17 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
<table>
<tbody>
<SurveyListItem
question={newItem}
isChecked={false}
isFirst
isLast
canEdit
/>
</tbody>
</table>
);
});
expect(wrapper.find('span[aria-label="Required"]').length).toBe(1);
@ -72,12 +108,19 @@ describe('<SurveyListItem />', () => {
test('items that are not required should not have an asterisk', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={item} isChecked={false} isFirst isLast />
wrapper = shallowWithContexts(
<SurveyListItem
question={item}
isChecked={false}
isFirst
isLast
canEdit
/>
);
});
expect(wrapper.find('span[aria-label="Required"]').length).toBe(0);
});
test('required item has required asterisk', () => {
const newItem = {
question_name: 'Foo',
@ -89,7 +132,17 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
<table>
<tbody>
<SurveyListItem
question={newItem}
isChecked={false}
isFirst
isLast
canEdit
/>
</tbody>
</table>
);
});
expect(wrapper.find('Chip').length).toBe(6);
@ -98,6 +151,7 @@ describe('<SurveyListItem />', () => {
.filter((chip) => chip.prop('isOverFlowChip') !== true)
.map((chip) => expect(chip.prop('isReadOnly')).toBe(true));
});
test('items that are no required should have no an asterisk', () => {
const newItem = {
question_name: 'Foo',
@ -109,24 +163,31 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem question={newItem} isChecked={false} isFirst isLast />
<table>
<tbody>
<SurveyListItem
question={newItem}
isChecked={false}
isFirst
isLast
canEdit
/>
</tbody>
</table>
);
});
expect(wrapper.find('span').text()).toBe('ENCRYPTED');
});
test('users without edit/delete permissions are unable to reorder the questions', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
wrapper = shallowWithContexts(
<SurveyListItem canEdit={false} question={item} isChecked={false} />
);
});
expect(wrapper.find('button[aria-label="move up"]').prop('disabled')).toBe(
true
);
expect(
wrapper.find('button[aria-label="move down"]').prop('disabled')
).toBe(true);
expect(wrapper.find('button[aria-label="move up"]')).toHaveLength(0);
expect(wrapper.find('button[aria-label="move down"]')).toHaveLength(0);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
});
@ -134,9 +195,20 @@ describe('<SurveyListItem />', () => {
let wrapper;
act(() => {
wrapper = mountWithContexts(
<SurveyListItem canEdit question={item} isChecked={false} />
<table>
<tbody>
<SurveyListItem
question={item}
isFirst
isLast
isChecked={false}
canEdit
/>
</tbody>
</table>
);
});
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
expect(wrapper.find('Button[ouiaId="edit-survey-buzz"]').prop('to')).toBe(
'survey/edit?question_variable=buzz'

View File

@ -16,14 +16,13 @@ import {
import { ToolbarAddButton } from 'components/PaginatedTable';
const Toolbar = styled(_Toolbar)`
margin-left: 52px;
margin-left: 10px;
`;
function SurveyToolbar({
canEdit,
isAllSelected,
onSelectAll,
surveyEnabled,
onToggleSurvey,
isDeleteDisabled,