diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx index 9eed8c8d66..58a094dace 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx @@ -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 ; } @@ -194,6 +214,37 @@ function ScheduleDetail({ schedule, i18n }) { )} + + {summary_fields?.user_capabilities?.edit && ( + + )} + {summary_fields?.user_capabilities?.delete && ( + + {i18n._(t`Delete`)} + + )} + + {deletionError && ( + setDeletionError(null)} + > + {i18n._(t`Failed to delete schedule.`)} + + + )} ); } diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx index d802399b7b..e0893f150a 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx @@ -66,7 +66,9 @@ describe('', () => { }); 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('', () => { }); 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( + } + />, + { + 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( + } + />, + { + 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); + }); });