1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 23:51:09 +03:00

Add Edit/Delete buttons to JT Schedule Details

Also, add unit-tests to the related changes.

Closes: https://github.com/ansible/awx/issues/6171
This commit is contained in:
nixocio 2020-04-22 16:15:32 -04:00
parent 72de660ea1
commit 271b19bf09
2 changed files with 143 additions and 5 deletions

View File

@ -1,12 +1,13 @@
import React, { useCallback, useEffect } from 'react';
import { Link } from 'react-router-dom';
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useHistory, useLocation } from 'react-router-dom';
import { RRule, rrulestr } from 'rrule';
import styled from 'styled-components';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Schedule } from '@types';
import { Chip, ChipGroup, Title } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { Chip, ChipGroup, Title, Button } from '@patternfly/react-core';
import AlertModal from '@components/AlertModal';
import { CardBody, CardActionsRow } from '@components/Card';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import CredentialChip from '@components/CredentialChip';
@ -15,6 +16,8 @@ import { ScheduleOccurrences, ScheduleToggle } from '@components/Schedule';
import { formatDateString } from '@util/dates';
import useRequest from '@util/useRequest';
import { SchedulesAPI } from '@api';
import DeleteButton from '@components/DeleteButton';
import ErrorDetail from '@components/ErrorDetail';
const PromptTitle = styled(Title)`
--pf-c-title--m-md--FontWeight: 700;
@ -42,6 +45,23 @@ function ScheduleDetail({ schedule, i18n }) {
timezone,
} = schedule;
const [deletionError, setDeletionError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(false);
const history = useHistory();
const { pathname } = useLocation();
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
const handleDelete = async () => {
setHasContentLoading(true);
try {
await SchedulesAPI.destroy(id);
history.push(`${pathRoot}schedules`);
} catch (error) {
setDeletionError(error);
}
setHasContentLoading(false);
};
const {
result: [credentials, preview],
isLoading,
@ -79,7 +99,7 @@ function ScheduleDetail({ schedule, i18n }) {
(job_tags && job_tags.length > 0) ||
(skip_tags && skip_tags.length > 0);
if (isLoading) {
if (isLoading || hasContentLoading) {
return <ContentLoading />;
}
@ -194,6 +214,37 @@ function ScheduleDetail({ schedule, i18n }) {
</>
)}
</DetailList>
<CardActionsRow>
{summary_fields?.user_capabilities?.edit && (
<Button
aria-label={i18n._(t`Edit`)}
component={Link}
to={pathname.replace('details', 'edit')}
>
{i18n._(t`Edit`)}
</Button>
)}
{summary_fields?.user_capabilities?.delete && (
<DeleteButton
name={name}
modalTitle={i18n._(t`Delete Schedule`)}
onConfirm={handleDelete}
>
{i18n._(t`Delete`)}
</DeleteButton>
)}
</CardActionsRow>
{deletionError && (
<AlertModal
isOpen={deletionError}
variant="danger"
title={i18n._(t`Error!`)}
onClose={() => setDeletionError(null)}
>
{i18n._(t`Failed to delete schedule.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
)}
</CardBody>
);
}

View File

@ -66,7 +66,9 @@ describe('<ScheduleDetail />', () => {
});
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});
test('details should render with the proper values without prompts', async () => {
SchedulesAPI.readCredentials.mockResolvedValueOnce({
data: {
@ -260,4 +262,89 @@ describe('<ScheduleDetail />', () => {
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
test('should show edit button for users with edit permission', async () => {
SchedulesAPI.readCredentials.mockResolvedValueOnce({
data: {
count: 0,
results: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/job_template/:id/schedules/:scheduleId"
component={() => <ScheduleDetail schedule={schedule} />}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match: { params: { id: 1 } },
},
},
},
}
);
});
const editButton = await waitForElement(
wrapper,
'ScheduleDetail Button[aria-label="Edit"]'
);
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe(
'/templates/job_template/1/schedules/1/edit'
);
});
test('Error dialog shown for failed deletion', async () => {
SchedulesAPI.destroy.mockImplementationOnce(() =>
Promise.reject(new Error())
);
SchedulesAPI.readCredentials.mockResolvedValueOnce({
data: {
count: 0,
results: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/job_template/:id/schedules/:scheduleId"
component={() => <ScheduleDetail schedule={schedule} />}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match: { params: { id: 1 } },
},
},
},
}
);
});
await act(async () => {
wrapper.find('DeleteButton').invoke('onConfirm')();
});
await waitForElement(
wrapper,
'Modal[title="Error!"]',
el => el.length === 1
);
await act(async () => {
wrapper.find('Modal[title="Error!"]').invoke('onClose')();
});
await waitForElement(
wrapper,
'Modal[title="Error!"]',
el => el.length === 0
);
expect(SchedulesAPI.destroy).toHaveBeenCalledTimes(1);
});
});