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);
+ });
});