mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
convert Schedules list to PaginatedTable
This commit is contained in:
parent
271eb4043a
commit
953fa3fe0d
@ -8,8 +8,14 @@
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"plugins": ["react-hooks", "jsx-a11y", "i18next"],
|
||||
"extends": ["airbnb", "prettier", "prettier/react", "plugin:jsx-a11y/strict", "plugin:i18next/recommended"],
|
||||
"plugins": ["react-hooks", "jsx-a11y", "i18next"],
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"prettier",
|
||||
"prettier/react",
|
||||
"plugin:jsx-a11y/strict",
|
||||
"plugin:i18next/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16.5.2"
|
||||
@ -24,7 +30,70 @@
|
||||
"window": true
|
||||
},
|
||||
"rules": {
|
||||
"i18next/no-literal-string": [2, {"markupOnly": true, "ignoreAttribute": ["to", "streamType", "path", "component", "variant", "key", "position", "promptName", "color","promptId", "headingLevel", "size", "target", "autoComplete","trigger", "from", "name", "fieldId", "css", "gutter", "dataCy", "tooltipMaxWidth", "mode", "aria-labelledby","aria-hidden","sortKey", "ouiaId", "credentialTypeNamespace", "link", "value", "credentialTypeKind", "linkTo", "scrollToAlignment", "displayKey", "sortedColumnKey", "maxHeight", "role", "aria-haspopup", "dropDirection", "resizeOrientation", "src", "theme"], "ignore":["Ansible", "Tower", "JSON", "YAML", "lg", "START"],"ignoreComponent":["code", "Omit","PotentialLink", "TypeRedirect", "Radio", "RunOnRadio", "NodeTypeLetter", "SelectableItem", "Dash"], "ignoreCallee": ["describe"] }],
|
||||
"i18next/no-literal-string": [
|
||||
2,
|
||||
{
|
||||
"markupOnly": true,
|
||||
"ignoreAttribute": [
|
||||
"to",
|
||||
"streamType",
|
||||
"path",
|
||||
"component",
|
||||
"variant",
|
||||
"key",
|
||||
"position",
|
||||
"promptName",
|
||||
"color",
|
||||
"promptId",
|
||||
"headingLevel",
|
||||
"size",
|
||||
"target",
|
||||
"autoComplete",
|
||||
"trigger",
|
||||
"from",
|
||||
"name",
|
||||
"fieldId",
|
||||
"css",
|
||||
"gutter",
|
||||
"dataCy",
|
||||
"tooltipMaxWidth",
|
||||
"mode",
|
||||
"aria-labelledby",
|
||||
"aria-hidden",
|
||||
"sortKey",
|
||||
"ouiaId",
|
||||
"credentialTypeNamespace",
|
||||
"link",
|
||||
"value",
|
||||
"credentialTypeKind",
|
||||
"linkTo",
|
||||
"scrollToAlignment",
|
||||
"displayKey",
|
||||
"sortedColumnKey",
|
||||
"maxHeight",
|
||||
"role",
|
||||
"aria-haspopup",
|
||||
"dropDirection",
|
||||
"resizeOrientation",
|
||||
"src",
|
||||
"theme",
|
||||
"gridColumns"
|
||||
],
|
||||
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "START"],
|
||||
"ignoreComponent": [
|
||||
"code",
|
||||
"Omit",
|
||||
"PotentialLink",
|
||||
"TypeRedirect",
|
||||
"Radio",
|
||||
"RunOnRadio",
|
||||
"NodeTypeLetter",
|
||||
"SelectableItem",
|
||||
"Dash"
|
||||
],
|
||||
"ignoreCallee": ["describe"]
|
||||
}
|
||||
],
|
||||
"camelcase": "off",
|
||||
"arrow-parens": "off",
|
||||
"comma-dangle": "off",
|
||||
|
||||
@ -9,7 +9,7 @@ const ActionsGrid = styled.div`
|
||||
align-items: center;
|
||||
|
||||
${props => {
|
||||
const columns = '40px '.repeat(props.numActions || 1);
|
||||
const columns = props.gridColumns || '40px '.repeat(props.numActions || 1);
|
||||
return css`
|
||||
grid-template-columns: ${columns};
|
||||
`;
|
||||
@ -17,7 +17,7 @@ const ActionsGrid = styled.div`
|
||||
`;
|
||||
ActionsGrid.displayName = 'ActionsGrid';
|
||||
|
||||
export default function ActionsTd({ children, ...props }) {
|
||||
export default function ActionsTd({ children, gridColumns, ...props }) {
|
||||
const numActions = children.length || 1;
|
||||
const width = numActions * 40;
|
||||
return (
|
||||
@ -28,7 +28,7 @@ export default function ActionsTd({ children, ...props }) {
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<ActionsGrid numActions={numActions}>
|
||||
<ActionsGrid numActions={numActions} gridColumns={gridColumns}>
|
||||
{React.Children.map(children, (child, i) =>
|
||||
React.cloneElement(child, {
|
||||
column: i + 1,
|
||||
|
||||
@ -6,11 +6,9 @@ import { t } from '@lingui/macro';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import AlertModal from '../../AlertModal';
|
||||
import ErrorDetail from '../../ErrorDetail';
|
||||
import PaginatedTable, { HeaderRow, HeaderCell } from '../../PaginatedTable';
|
||||
import DataListToolbar from '../../DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
ToolbarDeleteButton,
|
||||
} from '../../PaginatedDataList';
|
||||
import { ToolbarAddButton, ToolbarDeleteButton } from '../../PaginatedDataList';
|
||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import ScheduleListItem from './ScheduleListItem';
|
||||
@ -119,19 +117,27 @@ function ScheduleList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
<PaginatedTable
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading || isDeleteLoading}
|
||||
items={schedules}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
onRowClick={handleSelect}
|
||||
renderItem={item => (
|
||||
headerRow={
|
||||
<HeaderRow qsConfig={QS_CONFIG}>
|
||||
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
|
||||
<HeaderCell>{i18n._(t`Type`)}</HeaderCell>
|
||||
<HeaderCell sortKey="next_run">{i18n._(t`Next Run`)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
}
|
||||
renderRow={(item, index) => (
|
||||
<ScheduleListItem
|
||||
isSelected={selected.some(row => row.id === item.id)}
|
||||
key={item.id}
|
||||
onSelect={() => handleSelect(item)}
|
||||
schedule={item}
|
||||
rowIndex={index}
|
||||
/>
|
||||
)}
|
||||
toolbarSearchColumns={[
|
||||
@ -153,16 +159,6 @@ function ScheduleList({
|
||||
key: 'modified_by__username__icontains',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Next Run`),
|
||||
key: 'next_run',
|
||||
},
|
||||
]}
|
||||
toolbarSearchableKeys={searchableKeys}
|
||||
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||
renderToolbar={props => (
|
||||
|
||||
@ -59,44 +59,61 @@ describe('ScheduleList', () => {
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked
|
||||
wrapper
|
||||
.find('.pf-c-table__check')
|
||||
.first()
|
||||
.find('input')
|
||||
.props().checked
|
||||
).toBe(false);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DataListCheck[id="select-schedule-1"]')
|
||||
.find('.pf-c-table__check')
|
||||
.first()
|
||||
.find('input')
|
||||
.invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked
|
||||
wrapper
|
||||
.find('.pf-c-table__check')
|
||||
.first()
|
||||
.find('input')
|
||||
.props().checked
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DataListCheck[id="select-schedule-1"]')
|
||||
.find('.pf-c-table__check')
|
||||
.first()
|
||||
.find('input')
|
||||
.invoke('onChange')(false);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked
|
||||
wrapper
|
||||
.find('.pf-c-table__check')
|
||||
.first()
|
||||
.find('input')
|
||||
.props().checked
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should check all row items when select all is checked', async () => {
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(wrapper.find('.pf-c-table__check input')).toHaveLength(5);
|
||||
wrapper.find('.pf-c-table__check input').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
wrapper.find('.pf-c-table__check input').forEach(el => {
|
||||
expect(el.props().checked).toBe(true);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
wrapper.find('.pf-c-table__check input').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -104,7 +121,8 @@ describe('ScheduleList', () => {
|
||||
test('should call api delete schedules for each selected schedule', async () => {
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DataListCheck[id="select-schedule-3"]')
|
||||
.find('.pf-c-table__check input')
|
||||
.at(3)
|
||||
.invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
@ -122,7 +140,8 @@ describe('ScheduleList', () => {
|
||||
expect(wrapper.find('Modal').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DataListCheck[id="select-schedule-2"]')
|
||||
.find('.pf-c-table__check input')
|
||||
.at(2)
|
||||
.invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
@ -4,31 +4,16 @@ import { bool, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
DataListAction as _DataListAction,
|
||||
DataListCheck,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { Tr, Td } from '@patternfly/react-table';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import DataListCell from '../../DataListCell';
|
||||
import { DetailList, Detail } from '../../DetailList';
|
||||
import { ActionsTd, ActionItem } from '../../PaginatedTable';
|
||||
import { ScheduleToggle } from '..';
|
||||
import { Schedule } from '../../../types';
|
||||
import { formatDateString } from '../../../util/dates';
|
||||
|
||||
const DataListAction = styled(_DataListAction)`
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: 92px 40px;
|
||||
`;
|
||||
|
||||
function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
|
||||
function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) {
|
||||
const labelId = `check-action-${schedule.id}`;
|
||||
|
||||
const jobTypeLabels = {
|
||||
@ -62,69 +47,56 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<DataListItem
|
||||
key={schedule.id}
|
||||
aria-labelledby={labelId}
|
||||
id={`${schedule.id}`}
|
||||
>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
id={`select-schedule-${schedule.id}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="name">
|
||||
<Link to={`${scheduleBaseUrl}/details`}>
|
||||
<b>{schedule.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
<DataListCell key="type">
|
||||
{
|
||||
jobTypeLabels[
|
||||
schedule.summary_fields.unified_job_template.unified_job_type
|
||||
]
|
||||
}
|
||||
</DataListCell>,
|
||||
<DataListCell key="next_run">
|
||||
{schedule.next_run && (
|
||||
<DetailList stacked>
|
||||
<Detail
|
||||
label={i18n._(t`Next Run`)}
|
||||
value={formatDateString(schedule.next_run)}
|
||||
/>
|
||||
</DetailList>
|
||||
)}
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label={i18n._(t`actions`)}
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
key="actions"
|
||||
<Tr id={`schedule-row-${schedule.id}`}>
|
||||
<Td
|
||||
select={{
|
||||
rowIndex,
|
||||
isSelected,
|
||||
onSelect,
|
||||
disable: false,
|
||||
}}
|
||||
dataLabel={i18n._(`Selected`)}
|
||||
/>
|
||||
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
|
||||
<Link to={`${scheduleBaseUrl}/details`}>
|
||||
<b>{schedule.name}</b>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td dataLabel={i18n._(t`Type`)}>
|
||||
{
|
||||
jobTypeLabels[
|
||||
schedule.summary_fields.unified_job_template.unified_job_type
|
||||
]
|
||||
}
|
||||
</Td>
|
||||
<Td dataLabel={i18n._(t`Next Run`)}>
|
||||
{schedule.next_run && (
|
||||
<DetailList stacked>
|
||||
<Detail
|
||||
label={i18n._(t`Next Run`)}
|
||||
value={formatDateString(schedule.next_run)}
|
||||
/>
|
||||
</DetailList>
|
||||
)}
|
||||
</Td>
|
||||
<ActionsTd dataLabel={i18n._(t`Actions`)} gridColumns="auto 40px">
|
||||
<ScheduleToggle schedule={schedule} />
|
||||
<ActionItem
|
||||
visible={schedule.summary_fields.user_capabilities.edit}
|
||||
tooltip={i18n._(t`Edit Schedule`)}
|
||||
>
|
||||
<ScheduleToggle schedule={schedule} />
|
||||
{schedule.summary_fields.user_capabilities.edit ? (
|
||||
<Tooltip content={i18n._(t`Edit Schedule`)} position="top">
|
||||
<Button
|
||||
aria-label={i18n._(t`Edit Schedule`)}
|
||||
css="grid-column: 2"
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`${scheduleBaseUrl}/edit`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
<Button
|
||||
aria-label={i18n._(t`Edit Schedule`)}
|
||||
css="grid-column: 2"
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`${scheduleBaseUrl}/edit`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -50,39 +50,47 @@ describe('ScheduleListItem', () => {
|
||||
describe('User has edit permissions', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleListItem
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
schedule={mockSchedule}
|
||||
/>
|
||||
<table>
|
||||
<tbody>
|
||||
<ScheduleListItem
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
schedule={mockSchedule}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('Name correctly shown with correct link', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.first()
|
||||
.find('Td')
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe('Mock Schedule');
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.first()
|
||||
.find('Td')
|
||||
.at(1)
|
||||
.find('Link')
|
||||
.props().to
|
||||
).toBe('/templates/job_template/12/schedules/6/details');
|
||||
});
|
||||
|
||||
test('Type correctly shown', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.at(1)
|
||||
.find('Td')
|
||||
.at(2)
|
||||
.text()
|
||||
).toBe('Playbook Run');
|
||||
});
|
||||
|
||||
test('Edit button shown with correct link', () => {
|
||||
expect(wrapper.find('PencilAltIcon').length).toBe(1);
|
||||
expect(
|
||||
@ -92,6 +100,7 @@ describe('ScheduleListItem', () => {
|
||||
.props().to
|
||||
).toBe('/templates/job_template/12/schedules/6/edit');
|
||||
});
|
||||
|
||||
test('Toggle button enabled', () => {
|
||||
expect(
|
||||
wrapper
|
||||
@ -100,63 +109,74 @@ describe('ScheduleListItem', () => {
|
||||
.props().isDisabled
|
||||
).toBe(false);
|
||||
});
|
||||
test('Clicking checkbox makes expected callback', () => {
|
||||
|
||||
test('Clicking checkbox selects item', () => {
|
||||
wrapper
|
||||
.find('DataListCheck')
|
||||
.find('Td')
|
||||
.first()
|
||||
.find('input')
|
||||
.simulate('change');
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User has read-only permissions', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleListItem
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
schedule={{
|
||||
...mockSchedule,
|
||||
summary_fields: {
|
||||
...mockSchedule.summary_fields,
|
||||
user_capabilities: {
|
||||
edit: false,
|
||||
delete: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<table>
|
||||
<tbody>
|
||||
<ScheduleListItem
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
schedule={{
|
||||
...mockSchedule,
|
||||
summary_fields: {
|
||||
...mockSchedule.summary_fields,
|
||||
user_capabilities: {
|
||||
edit: false,
|
||||
delete: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('Name correctly shown with correct link', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.first()
|
||||
.find('Td')
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe('Mock Schedule');
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.first()
|
||||
.find('Td')
|
||||
.at(1)
|
||||
.find('Link')
|
||||
.props().to
|
||||
).toBe('/templates/job_template/12/schedules/6/details');
|
||||
});
|
||||
|
||||
test('Type correctly shown', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.at(1)
|
||||
.find('Td')
|
||||
.at(2)
|
||||
.text()
|
||||
).toBe('Playbook Run');
|
||||
});
|
||||
|
||||
test('Edit button hidden', () => {
|
||||
expect(wrapper.find('PencilAltIcon').length).toBe(0);
|
||||
});
|
||||
|
||||
test('Toggle button disabled', () => {
|
||||
expect(
|
||||
wrapper
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user