mirror of
https://github.com/ansible/awx.git
synced 2026-04-14 06:29:25 -02:30
Move approval action buttons from rows to to level list actions. UX updates to the display of the status.
This commit is contained in:
@@ -44,6 +44,11 @@ function getRouteConfig(i18n) {
|
|||||||
path: '/schedules',
|
path: '/schedules',
|
||||||
screen: Schedules,
|
screen: Schedules,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Workflow Approvals`),
|
||||||
|
path: '/workflow_approvals',
|
||||||
|
screen: WorkflowApprovals,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -127,11 +132,6 @@ function getRouteConfig(i18n) {
|
|||||||
path: '/applications',
|
path: '/applications',
|
||||||
screen: Applications,
|
screen: Applications,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: i18n._(t`Workflow Approvals`),
|
|
||||||
path: '/workflow_approvals',
|
|
||||||
screen: WorkflowApprovals,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
import DeleteButton from '../../../components/DeleteButton';
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
@@ -11,19 +12,19 @@ import {
|
|||||||
UserDateDetail,
|
UserDateDetail,
|
||||||
} from '../../../components/DetailList';
|
} from '../../../components/DetailList';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import WorkflowApprovalActionButtons from '../shared/WorkflowApprovalActionButtons';
|
|
||||||
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
|
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
|
||||||
import { formatDateString, secondsToHHMMSS } from '../../../util/dates';
|
import { formatDateString, secondsToHHMMSS } from '../../../util/dates';
|
||||||
import { WorkflowApprovalsAPI } from '../../../api';
|
import { WorkflowApprovalsAPI } from '../../../api';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { WorkflowApproval } from '../../../types';
|
||||||
|
|
||||||
function WorkflowApprovalDetail({ i18n, workflowApproval }) {
|
function WorkflowApprovalDetail({ i18n, workflowApproval }) {
|
||||||
const { id: workflowApprovalId } = useParams();
|
const { id: workflowApprovalId } = useParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
request: deleteWorkflowApproval,
|
request: deleteWorkflowApproval,
|
||||||
isLoading,
|
isLoading: isDeleteLoading,
|
||||||
error: deleteError,
|
error: deleteApprovalError,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
await WorkflowApprovalsAPI.destroy(workflowApprovalId);
|
await WorkflowApprovalsAPI.destroy(workflowApprovalId);
|
||||||
@@ -31,7 +32,44 @@ function WorkflowApprovalDetail({ i18n, workflowApproval }) {
|
|||||||
}, [workflowApprovalId, history])
|
}, [workflowApprovalId, history])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
const {
|
||||||
|
error: deleteError,
|
||||||
|
dismissError: dismissDeleteError,
|
||||||
|
} = useDismissableError(deleteApprovalError);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: approveApprovalError,
|
||||||
|
isLoading: isApproveLoading,
|
||||||
|
request: approveWorkflowApproval,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await WorkflowApprovalsAPI.approve(workflowApprovalId);
|
||||||
|
history.push(`/workflow_approvals/${workflowApprovalId}`);
|
||||||
|
}, [workflowApprovalId, history]),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: approveError,
|
||||||
|
dismissError: dismissApproveError,
|
||||||
|
} = useDismissableError(approveApprovalError);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: denyApprovalError,
|
||||||
|
isLoading: isDenyLoading,
|
||||||
|
request: denyWorkflowApproval,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await WorkflowApprovalsAPI.deny(workflowApprovalId);
|
||||||
|
history.push(`/workflow_approvals/${workflowApprovalId}`);
|
||||||
|
}, [workflowApprovalId, history]),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: denyError,
|
||||||
|
dismissError: dismissDenyError,
|
||||||
|
} = useDismissableError(denyApprovalError);
|
||||||
|
|
||||||
const sourceWorkflowJob =
|
const sourceWorkflowJob =
|
||||||
workflowApproval?.summary_fields?.source_workflow_job;
|
workflowApproval?.summary_fields?.source_workflow_job;
|
||||||
@@ -39,9 +77,7 @@ function WorkflowApprovalDetail({ i18n, workflowApproval }) {
|
|||||||
const sourceWorkflowJobTemplate =
|
const sourceWorkflowJobTemplate =
|
||||||
workflowApproval?.summary_fields?.workflow_job_template;
|
workflowApproval?.summary_fields?.workflow_job_template;
|
||||||
|
|
||||||
const handleSuccesfulAction = useCallback(() => {
|
const isLoading = isDeleteLoading || isApproveLoading || isDenyLoading;
|
||||||
history.push(`/workflow_approvals/${workflowApprovalId}`);
|
|
||||||
}, [history, workflowApprovalId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
@@ -135,11 +171,24 @@ function WorkflowApprovalDetail({ i18n, workflowApproval }) {
|
|||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{workflowApproval.can_approve_or_deny && (
|
{workflowApproval.can_approve_or_deny && (
|
||||||
<WorkflowApprovalActionButtons
|
<>
|
||||||
icon={false}
|
<Button
|
||||||
workflowApproval={workflowApproval}
|
aria-label={i18n._(t`Approve`)}
|
||||||
onSuccessfulAction={handleSuccesfulAction}
|
variant="primary"
|
||||||
/>
|
onClick={approveWorkflowApproval}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Approve`)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`Deny`)}
|
||||||
|
variant="danger"
|
||||||
|
onClick={denyWorkflowApproval}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Deny`)}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{workflowApproval.summary_fields.user_capabilities &&
|
{workflowApproval.summary_fields.user_capabilities &&
|
||||||
workflowApproval.summary_fields.user_capabilities.delete && (
|
workflowApproval.summary_fields.user_capabilities.delete && (
|
||||||
@@ -153,19 +202,45 @@ function WorkflowApprovalDetail({ i18n, workflowApproval }) {
|
|||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
)}
|
)}
|
||||||
</CardActionsRow>
|
</CardActionsRow>
|
||||||
{error && (
|
{deleteError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={error}
|
isOpen={deleteError}
|
||||||
variant="error"
|
variant="error"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={dismissError}
|
onClose={dismissDeleteError}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete workflow approval.`)}
|
{i18n._(t`Failed to delete workflow approval.`)}
|
||||||
<ErrorDetail error={error} />
|
<ErrorDetail error={deleteError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
{approveError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={approveError}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissApproveError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to approve workflow approval.`)}
|
||||||
|
<ErrorDetail error={approveError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
{denyError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={denyError}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissDenyError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to deny workflow approval.`)}
|
||||||
|
<ErrorDetail error={denyError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowApprovalDetail.defaultProps = {
|
||||||
|
workflowApproval: WorkflowApproval.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(WorkflowApprovalDetail);
|
export default withI18n()(WorkflowApprovalDetail);
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ describe('<WorkflowApprovalDetail />', () => {
|
|||||||
);
|
);
|
||||||
assertDetail('Last Modified', formatDateString(workflowApproval.modified));
|
assertDetail('Last Modified', formatDateString(workflowApproval.modified));
|
||||||
assertDetail('Elapsed', '00:00:22');
|
assertDetail('Elapsed', '00:00:22');
|
||||||
expect(wrapper.find('WorkflowApprovalActionButtons').length).toBe(1);
|
expect(wrapper.find('Button[aria-label="Approve"]').length).toBe(1);
|
||||||
|
expect(wrapper.find('Button[aria-label="Deny"]').length).toBe(1);
|
||||||
expect(wrapper.find('DeleteButton').length).toBe(1);
|
expect(wrapper.find('DeleteButton').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,6 +160,66 @@ describe('<WorkflowApprovalDetail />', () => {
|
|||||||
expect(wrapper.find('WorkflowApprovalActionButtons').length).toBe(0);
|
expect(wrapper.find('WorkflowApprovalActionButtons').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Error dialog shown for failed approval', async () => {
|
||||||
|
WorkflowApprovalsAPI.approve.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalDetail workflowApproval={workflowApproval} />
|
||||||
|
);
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'WorkflowApprovalDetail Button[aria-label="Approve"]'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Button[aria-label="Approve"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(WorkflowApprovalsAPI.approve).toHaveBeenCalledTimes(1);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Error dialog shown for failed denial', async () => {
|
||||||
|
WorkflowApprovalsAPI.deny.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalDetail workflowApproval={workflowApproval} />
|
||||||
|
);
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'WorkflowApprovalDetail Button[aria-label="Deny"]'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Button[aria-label="Deny"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(WorkflowApprovalsAPI.deny).toHaveBeenCalledTimes(1);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('delete button should be hidden when user cannot delete', () => {
|
test('delete button should be hidden when user cannot delete', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<WorkflowApprovalDetail
|
<WorkflowApprovalDetail
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import AlertModal from '../../../components/AlertModal';
|
|||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
||||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
import useRequest, {
|
||||||
|
useDeleteItems,
|
||||||
|
useDismissableError,
|
||||||
|
} from '../../../util/useRequest';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import useWsWorkflowApprovals from './useWsWorkflowApprovals';
|
import useWsWorkflowApprovals from './useWsWorkflowApprovals';
|
||||||
|
import WorkflowApprovalListApproveButton from './WorkflowApprovalListApproveButton';
|
||||||
|
import WorkflowApprovalListDenyButton from './WorkflowApprovalListDenyButton';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('workflow_approvals', {
|
const QS_CONFIG = getQSConfig('workflow_approvals', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -106,13 +111,64 @@ function WorkflowApprovalsList({ i18n }) {
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: approveApprovalError,
|
||||||
|
isLoading: isApproveLoading,
|
||||||
|
request: approveWorkflowApprovals,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(
|
||||||
|
selected.map(({ id }) => WorkflowApprovalsAPI.approve(id))
|
||||||
|
);
|
||||||
|
}, [selected]),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
await approveWorkflowApprovals();
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: approveError,
|
||||||
|
dismissError: dismissApproveError,
|
||||||
|
} = useDismissableError(approveApprovalError);
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: denyApprovalError,
|
||||||
|
isLoading: isDenyLoading,
|
||||||
|
request: denyWorkflowApprovals,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(
|
||||||
|
selected.map(({ id }) => WorkflowApprovalsAPI.deny(id))
|
||||||
|
);
|
||||||
|
}, [selected]),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeny = async () => {
|
||||||
|
await denyWorkflowApprovals();
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: denyError,
|
||||||
|
dismissError: dismissDenyError,
|
||||||
|
} = useDismissableError(denyApprovalError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isWorkflowApprovalsLoading || isDeleteLoading}
|
hasContentLoading={
|
||||||
|
isWorkflowApprovalsLoading ||
|
||||||
|
isDeleteLoading ||
|
||||||
|
isApproveLoading ||
|
||||||
|
isDenyLoading
|
||||||
|
}
|
||||||
items={workflowApprovals}
|
items={workflowApprovals}
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
pluralizedItemName={i18n._(t`Workflow Approvals`)}
|
pluralizedItemName={i18n._(t`Workflow Approvals`)}
|
||||||
@@ -147,6 +203,16 @@ function WorkflowApprovalsList({ i18n }) {
|
|||||||
}
|
}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
|
<WorkflowApprovalListApproveButton
|
||||||
|
key="approve"
|
||||||
|
onApprove={handleApprove}
|
||||||
|
selectedItems={selected}
|
||||||
|
/>,
|
||||||
|
<WorkflowApprovalListDenyButton
|
||||||
|
key="deny"
|
||||||
|
onDeny={handleDeny}
|
||||||
|
selectedItems={selected}
|
||||||
|
/>,
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
@@ -171,15 +237,39 @@ function WorkflowApprovalsList({ i18n }) {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
<AlertModal
|
{deletionError && (
|
||||||
isOpen={deletionError}
|
<AlertModal
|
||||||
variant="error"
|
isOpen={deletionError}
|
||||||
title={i18n._(t`Error!`)}
|
variant="error"
|
||||||
onClose={clearDeletionError}
|
title={i18n._(t`Error!`)}
|
||||||
>
|
onClose={clearDeletionError}
|
||||||
{i18n._(t`Failed to delete one or more workflow approval.`)}
|
>
|
||||||
<ErrorDetail error={deletionError} />
|
{i18n._(t`Failed to delete one or more workflow approval.`)}
|
||||||
</AlertModal>
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
{approveError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={approveError}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissApproveError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to approve one or more workflow approval.`)}
|
||||||
|
<ErrorDetail error={approveError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
{denyError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={denyError}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissDenyError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to deny one or more workflow approval.`)}
|
||||||
|
<ErrorDetail error={denyError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||||
|
import { KebabifiedContext } from '../../../contexts/Kebabified';
|
||||||
|
import { WorkflowApproval } from '../../../types';
|
||||||
|
|
||||||
|
function cannotApprove(item) {
|
||||||
|
return !item.can_approve_or_deny;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkflowApprovalListApproveButton({ onApprove, selectedItems, i18n }) {
|
||||||
|
const { isKebabified } = useContext(KebabifiedContext);
|
||||||
|
|
||||||
|
const renderTooltip = () => {
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
return i18n._(t`Select a row to approve`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsUnableToApprove = selectedItems
|
||||||
|
.filter(cannotApprove)
|
||||||
|
.map(item => item.name)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
if (selectedItems.some(cannotApprove)) {
|
||||||
|
return i18n._(
|
||||||
|
t`You are unable to act on the following workflow approvals: ${itemsUnableToApprove}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n._(t`Approve`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
selectedItems.length === 0 || selectedItems.some(cannotApprove);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isKebabified ? (
|
||||||
|
<DropdownItem
|
||||||
|
key="approve"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
component="button"
|
||||||
|
onClick={onApprove}
|
||||||
|
>
|
||||||
|
{i18n._(t`Approve`)}
|
||||||
|
</DropdownItem>
|
||||||
|
) : (
|
||||||
|
<Tooltip content={renderTooltip()} position="top">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
aria-label={i18n._(t`Approve`)}
|
||||||
|
variant="primary"
|
||||||
|
onClick={onApprove}
|
||||||
|
>
|
||||||
|
{i18n._(t`Approve`)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowApprovalListApproveButton.propTypes = {
|
||||||
|
onApprove: PropTypes.func.isRequired,
|
||||||
|
selectedItems: PropTypes.arrayOf(WorkflowApproval),
|
||||||
|
};
|
||||||
|
|
||||||
|
WorkflowApprovalListApproveButton.defaultProps = {
|
||||||
|
selectedItems: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(WorkflowApprovalListApproveButton);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import WorkflowApprovalListApproveButton from './WorkflowApprovalListApproveButton';
|
||||||
|
|
||||||
|
const workflowApproval = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
can_approve_or_deny: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<WorkflowApprovalListApproveButton />', () => {
|
||||||
|
test('should render button', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListApproveButton
|
||||||
|
onApprove={() => {}}
|
||||||
|
selectedItems={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should invoke onApprove prop', () => {
|
||||||
|
const onApprove = jest.fn();
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListApproveButton
|
||||||
|
onApprove={onApprove}
|
||||||
|
selectedItems={[workflowApproval]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
expect(onApprove).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable button when no approve/deny permissions', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListApproveButton
|
||||||
|
onApprove={() => {}}
|
||||||
|
selectedItems={[{ ...workflowApproval, can_approve_or_deny: false }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button[disabled]')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render tooltip', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListApproveButton
|
||||||
|
onApprove={() => {}}
|
||||||
|
selectedItems={[workflowApproval]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Tooltip')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Tooltip').prop('content')).toEqual('Approve');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||||
|
import { KebabifiedContext } from '../../../contexts/Kebabified';
|
||||||
|
import { WorkflowApproval } from '../../../types';
|
||||||
|
|
||||||
|
function cannotDeny(item) {
|
||||||
|
return !item.can_approve_or_deny;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkflowApprovalListDenyButton({ onDeny, selectedItems, i18n }) {
|
||||||
|
const { isKebabified } = useContext(KebabifiedContext);
|
||||||
|
|
||||||
|
const renderTooltip = () => {
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
return i18n._(t`Select a row to deny`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsUnableToDeny = selectedItems
|
||||||
|
.filter(cannotDeny)
|
||||||
|
.map(item => item.name)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
if (selectedItems.some(cannotDeny)) {
|
||||||
|
return i18n._(
|
||||||
|
t`You are unable to act on the following workflow approvals: ${itemsUnableToDeny}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n._(t`Deny`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
selectedItems.length === 0 || selectedItems.some(cannotDeny);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isKebabified ? (
|
||||||
|
<DropdownItem
|
||||||
|
key="deny"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
component="button"
|
||||||
|
onClick={onDeny}
|
||||||
|
>
|
||||||
|
{i18n._(t`Deny`)}
|
||||||
|
</DropdownItem>
|
||||||
|
) : (
|
||||||
|
<Tooltip content={renderTooltip()} position="top">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
aria-label={i18n._(t`Deny`)}
|
||||||
|
variant="danger"
|
||||||
|
onClick={onDeny}
|
||||||
|
>
|
||||||
|
{i18n._(t`Deny`)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowApprovalListDenyButton.propTypes = {
|
||||||
|
onDeny: PropTypes.func.isRequired,
|
||||||
|
selectedItems: PropTypes.arrayOf(WorkflowApproval),
|
||||||
|
};
|
||||||
|
|
||||||
|
WorkflowApprovalListDenyButton.defaultProps = {
|
||||||
|
selectedItems: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(WorkflowApprovalListDenyButton);
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import WorkflowApprovalListDenyButton from './WorkflowApprovalListDenyButton';
|
||||||
|
|
||||||
|
const workflowApproval = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
can_approve_or_deny: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<WorkflowApprovalListDenyButton />', () => {
|
||||||
|
test('should render button', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListDenyButton onDeny={() => {}} selectedItems={[]} />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should invoke onDeny prop', () => {
|
||||||
|
const onDeny = jest.fn();
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListDenyButton
|
||||||
|
onDeny={onDeny}
|
||||||
|
selectedItems={[workflowApproval]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
wrapper.find('button').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
expect(onDeny).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable button when no approve/deny permissions', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListDenyButton
|
||||||
|
onDeny={() => {}}
|
||||||
|
selectedItems={[{ ...workflowApproval, can_approve_or_deny: false }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button[disabled]')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render tooltip', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListDenyButton
|
||||||
|
onDeny={() => {}}
|
||||||
|
selectedItems={[workflowApproval]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Tooltip')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('Tooltip').prop('content')).toEqual('Deny');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { string, bool, func } from 'prop-types';
|
import { string, bool, func } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
DataListAction as _DataListAction,
|
|
||||||
DataListCheck,
|
DataListCheck,
|
||||||
DataListItem,
|
DataListItem,
|
||||||
DataListItemCells,
|
DataListItemCells,
|
||||||
DataListItemRow,
|
DataListItemRow,
|
||||||
|
Label,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import DataListCell from '../../../components/DataListCell';
|
import DataListCell from '../../../components/DataListCell';
|
||||||
import { WorkflowApproval } from '../../../types';
|
import { WorkflowApproval } from '../../../types';
|
||||||
import { formatDateString } from '../../../util/dates';
|
import { formatDateString } from '../../../util/dates';
|
||||||
import WorkflowApprovalActionButtons from '../shared/WorkflowApprovalActionButtons';
|
|
||||||
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
|
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
|
||||||
|
|
||||||
const DataListAction = styled(_DataListAction)`
|
const StatusCell = styled(DataListCell)`
|
||||||
align-items: center;
|
@media screen and (min-width: 768px) {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-gap: 16px;
|
justify-content: flex-end;
|
||||||
grid-template-columns: repeat(2, 40px);
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const JobLabel = styled.b`
|
const JobLabel = styled.b`
|
||||||
@@ -35,7 +34,6 @@ function WorkflowApprovalListItem({
|
|||||||
detailUrl,
|
detailUrl,
|
||||||
i18n,
|
i18n,
|
||||||
}) {
|
}) {
|
||||||
const [actionTaken, setActionTaken] = useState(false);
|
|
||||||
const labelId = `check-action-${workflowApproval.id}`;
|
const labelId = `check-action-${workflowApproval.id}`;
|
||||||
const workflowJob = workflowApproval?.summary_fields?.source_workflow_job;
|
const workflowJob = workflowApproval?.summary_fields?.source_workflow_job;
|
||||||
|
|
||||||
@@ -44,23 +42,25 @@ function WorkflowApprovalListItem({
|
|||||||
workflowApproval.status === 'pending' &&
|
workflowApproval.status === 'pending' &&
|
||||||
workflowApproval.approval_expiration
|
workflowApproval.approval_expiration
|
||||||
) {
|
) {
|
||||||
return i18n._(
|
return (
|
||||||
t`Expires on ${formatDateString(workflowApproval.approval_expiration)}`
|
<Label>
|
||||||
|
{i18n._(
|
||||||
|
t`Expires on ${formatDateString(
|
||||||
|
workflowApproval.approval_expiration
|
||||||
|
)}`
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
workflowApproval.status === 'pending' &&
|
workflowApproval.status === 'pending' &&
|
||||||
!workflowApproval.approval_expiration
|
!workflowApproval.approval_expiration
|
||||||
) {
|
) {
|
||||||
return i18n._(t`Never expires`);
|
return <Label>{i18n._(t`Never expires`)}</Label>;
|
||||||
}
|
}
|
||||||
return <WorkflowApprovalStatus workflowApproval={workflowApproval} />;
|
return <WorkflowApprovalStatus workflowApproval={workflowApproval} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuccesfulAction = useCallback(() => {
|
|
||||||
setActionTaken(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem
|
<DataListItem
|
||||||
key={workflowApproval.id}
|
key={workflowApproval.id}
|
||||||
@@ -91,19 +91,9 @@ function WorkflowApprovalListItem({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell key="status">{getStatus()}</DataListCell>,
|
<StatusCell key="status">{getStatus()}</StatusCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<DataListAction aria-label="actions" aria-labelledby={labelId}>
|
|
||||||
{workflowApproval.can_approve_or_deny && !actionTaken ? (
|
|
||||||
<WorkflowApprovalActionButtons
|
|
||||||
workflowApproval={workflowApproval}
|
|
||||||
onSuccessfulAction={handleSuccesfulAction}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</DataListAction>
|
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
||||||
import { WorkflowApprovalsAPI } from '../../../api';
|
|
||||||
import workflowApproval from '../data.workflowApproval.json';
|
import workflowApproval from '../data.workflowApproval.json';
|
||||||
|
|
||||||
jest.mock('../../../api/models/WorkflowApprovals');
|
jest.mock('../../../api/models/WorkflowApprovals');
|
||||||
|
|
||||||
describe('<WorkflowApprovalListItem />', () => {
|
describe('<WorkflowApprovalListItem />', () => {
|
||||||
test('action buttons shown to users with ability to approve/deny', () => {
|
let wrapper;
|
||||||
const wrapper = mountWithContexts(
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
test('should display never expires status', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
<WorkflowApprovalListItem
|
<WorkflowApprovalListItem
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
||||||
@@ -18,38 +19,83 @@ describe('<WorkflowApprovalListItem />', () => {
|
|||||||
workflowApproval={workflowApproval}
|
workflowApproval={workflowApproval}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('WorkflowApprovalActionButtons').exists()).toBeTruthy();
|
expect(wrapper.find('Label[children="Never expires"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
test('should display timed out status', () => {
|
||||||
test('action buttons hidden from users without ability to approve/deny', () => {
|
wrapper = mountWithContexts(
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalListItem
|
<WorkflowApprovalListItem
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
workflowApproval={{ ...workflowApproval, can_approve_or_deny: false }}
|
workflowApproval={{
|
||||||
|
...workflowApproval,
|
||||||
|
status: 'failed',
|
||||||
|
timed_out: true,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('WorkflowApprovalActionButtons').exists()).toBeFalsy();
|
expect(wrapper.find('Label[children="Timed out"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
test('should display canceled status', () => {
|
||||||
test('should hide action buttons after successful action', async () => {
|
wrapper = mountWithContexts(
|
||||||
WorkflowApprovalsAPI.approve.mockResolvedValue();
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalListItem
|
<WorkflowApprovalListItem
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
workflowApproval={workflowApproval}
|
workflowApproval={{
|
||||||
|
...workflowApproval,
|
||||||
|
canceled_on: '2020-10-09T19:59:26.974046Z',
|
||||||
|
status: 'canceled',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('WorkflowApprovalActionButtons').exists()).toBeTruthy();
|
expect(wrapper.find('Label[children="Canceled"]').length).toBe(1);
|
||||||
await act(async () =>
|
});
|
||||||
wrapper.find('Button[aria-label="Approve"]').prop('onClick')()
|
test('should display approved status', () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
||||||
|
onSelect={() => {}}
|
||||||
|
workflowApproval={{
|
||||||
|
...workflowApproval,
|
||||||
|
status: 'successful',
|
||||||
|
summary_fields: {
|
||||||
|
...workflowApproval.summary_fields,
|
||||||
|
approved_or_denied_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
wrapper.update();
|
expect(wrapper.find('Label[children="Approved"]').length).toBe(1);
|
||||||
expect(WorkflowApprovalsAPI.approve).toHaveBeenCalled();
|
});
|
||||||
expect(wrapper.find('WorkflowApprovalActionButtons').exists()).toBeFalsy();
|
test('should display denied status', () => {
|
||||||
jest.clearAllMocks();
|
wrapper = mountWithContexts(
|
||||||
|
<WorkflowApprovalListItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl={`/workflow_approvals/${workflowApproval.id}`}
|
||||||
|
onSelect={() => {}}
|
||||||
|
workflowApproval={{
|
||||||
|
...workflowApproval,
|
||||||
|
failed: true,
|
||||||
|
status: 'failed',
|
||||||
|
summary_fields: {
|
||||||
|
...workflowApproval.summary_fields,
|
||||||
|
approved_or_denied_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Label[children="Denied"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button, Tooltip } from '@patternfly/react-core';
|
|
||||||
import { CheckIcon, CloseIcon } from '@patternfly/react-icons';
|
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
|
||||||
import AlertModal from '../../../components/AlertModal/AlertModal';
|
|
||||||
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
|
|
||||||
import { WorkflowApprovalsAPI } from '../../../api';
|
|
||||||
|
|
||||||
function WorkflowApprovalActionButtons({
|
|
||||||
workflowApproval,
|
|
||||||
icon,
|
|
||||||
i18n,
|
|
||||||
onSuccessfulAction,
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
isLoading: approveApprovalLoading,
|
|
||||||
error: approveApprovalError,
|
|
||||||
request: approveApprovalNode,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
|
||||||
await WorkflowApprovalsAPI.approve(workflowApproval.id);
|
|
||||||
if (onSuccessfulAction) {
|
|
||||||
onSuccessfulAction();
|
|
||||||
}
|
|
||||||
}, [onSuccessfulAction, workflowApproval.id]),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLoading: denyApprovalLoading,
|
|
||||||
error: denyApprovalError,
|
|
||||||
request: denyApprovalNode,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
|
||||||
await WorkflowApprovalsAPI.deny(workflowApproval.id);
|
|
||||||
if (onSuccessfulAction) {
|
|
||||||
onSuccessfulAction();
|
|
||||||
}
|
|
||||||
}, [onSuccessfulAction, workflowApproval.id]),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
error: approveError,
|
|
||||||
dismissError: dismissApproveError,
|
|
||||||
} = useDismissableError(approveApprovalError);
|
|
||||||
|
|
||||||
const {
|
|
||||||
error: denyError,
|
|
||||||
dismissError: dismissDenyError,
|
|
||||||
} = useDismissableError(denyApprovalError);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip content={i18n._(t`Approve`)} position="top">
|
|
||||||
<Button
|
|
||||||
isDisabled={approveApprovalLoading || denyApprovalLoading}
|
|
||||||
aria-label={i18n._(t`Approve`)}
|
|
||||||
variant={icon ? 'plain' : 'primary'}
|
|
||||||
onClick={approveApprovalNode}
|
|
||||||
>
|
|
||||||
{icon ? <CheckIcon /> : i18n._(t`Approve`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content={i18n._(t`Deny`)} position="top">
|
|
||||||
<Button
|
|
||||||
isDisabled={approveApprovalLoading || denyApprovalLoading}
|
|
||||||
aria-label={i18n._(t`Deny`)}
|
|
||||||
variant={icon ? 'plain' : 'danger'}
|
|
||||||
onClick={denyApprovalNode}
|
|
||||||
>
|
|
||||||
{icon ? <CloseIcon /> : i18n._(t`Deny`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
{approveError && (
|
|
||||||
<AlertModal
|
|
||||||
isOpen={approveError}
|
|
||||||
variant="error"
|
|
||||||
title={i18n._(t`Error!`)}
|
|
||||||
onClose={dismissApproveError}
|
|
||||||
>
|
|
||||||
{i18n._(t`Failed to approve this approval node.`)}
|
|
||||||
<ErrorDetail error={approveError} />
|
|
||||||
</AlertModal>
|
|
||||||
)}
|
|
||||||
{denyError && (
|
|
||||||
<AlertModal
|
|
||||||
isOpen={denyError}
|
|
||||||
variant="error"
|
|
||||||
title={i18n._(t`Error!`)}
|
|
||||||
onClose={dismissDenyError}
|
|
||||||
>
|
|
||||||
{i18n._(t`Failed to deny this approval node.`)}
|
|
||||||
<ErrorDetail error={denyError} />
|
|
||||||
</AlertModal>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkflowApprovalActionButtons.propTypes = {
|
|
||||||
workflowApproval: PropTypes.shape({}).isRequired,
|
|
||||||
icon: PropTypes.bool,
|
|
||||||
onSuccessfulAction: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
WorkflowApprovalActionButtons.defaultProps = {
|
|
||||||
icon: true,
|
|
||||||
onSuccessfulAction: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withI18n()(WorkflowApprovalActionButtons);
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
|
||||||
import { WorkflowApprovalsAPI } from '../../../api';
|
|
||||||
import WorkflowApprovalActionButtons from './WorkflowApprovalActionButtons';
|
|
||||||
import workflowApproval from '../data.workflowApproval.json';
|
|
||||||
|
|
||||||
jest.mock('../../../api/models/WorkflowApprovals');
|
|
||||||
|
|
||||||
describe('<WorkflowApprovalActionButtons />', () => {
|
|
||||||
let wrapper;
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.unmount();
|
|
||||||
});
|
|
||||||
test('initially renders succesfully with icons', () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalActionButtons workflowApproval={workflowApproval} />
|
|
||||||
);
|
|
||||||
expect(wrapper.find('CheckIcon').length).toBe(1);
|
|
||||||
expect(wrapper.find('CloseIcon').length).toBe(1);
|
|
||||||
expect(wrapper.find('Button[children="Approve"]').length).toBe(0);
|
|
||||||
expect(wrapper.find('Button[children="Deny"]').length).toBe(0);
|
|
||||||
});
|
|
||||||
test('initially renders succesfully without icons', () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalActionButtons
|
|
||||||
workflowApproval={workflowApproval}
|
|
||||||
icon={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('CheckIcon').length).toBe(0);
|
|
||||||
expect(wrapper.find('CloseIcon').length).toBe(0);
|
|
||||||
expect(wrapper.find('Button[children="Approve"]').length).toBe(1);
|
|
||||||
expect(wrapper.find('Button[children="Deny"]').length).toBe(1);
|
|
||||||
});
|
|
||||||
test('approving makes correct call with correct param', async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalActionButtons workflowApproval={workflowApproval} />
|
|
||||||
);
|
|
||||||
await act(async () => wrapper.find('CheckIcon').simulate('click'));
|
|
||||||
expect(WorkflowApprovalsAPI.approve).toHaveBeenCalledWith(
|
|
||||||
workflowApproval.id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('denying makes correct call with correct param', async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalActionButtons workflowApproval={workflowApproval} />
|
|
||||||
);
|
|
||||||
await act(async () => wrapper.find('CloseIcon').simulate('click'));
|
|
||||||
expect(WorkflowApprovalsAPI.deny).toHaveBeenCalledWith(workflowApproval.id);
|
|
||||||
});
|
|
||||||
test('approval error shown', async () => {
|
|
||||||
WorkflowApprovalsAPI.approve.mockRejectedValueOnce(
|
|
||||||
new Error({
|
|
||||||
response: {
|
|
||||||
config: {
|
|
||||||
method: 'post',
|
|
||||||
url: '/api/v2/workflow_approvals/approve',
|
|
||||||
},
|
|
||||||
data: 'An error occurred',
|
|
||||||
status: 403,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalActionButtons workflowApproval={workflowApproval} />
|
|
||||||
);
|
|
||||||
expect(wrapper.find('AlertModal').length).toBe(0);
|
|
||||||
await act(async () => wrapper.find('CheckIcon').simulate('click'));
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.find('AlertModal').length).toBe(1);
|
|
||||||
});
|
|
||||||
test('denial error shown', async () => {
|
|
||||||
WorkflowApprovalsAPI.deny.mockRejectedValueOnce(
|
|
||||||
new Error({
|
|
||||||
response: {
|
|
||||||
config: {
|
|
||||||
method: 'post',
|
|
||||||
url: '/api/v2/workflow_approvals/deny',
|
|
||||||
},
|
|
||||||
data: 'An error occurred',
|
|
||||||
status: 403,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<WorkflowApprovalActionButtons workflowApproval={workflowApproval} />
|
|
||||||
);
|
|
||||||
expect(wrapper.find('AlertModal').length).toBe(0);
|
|
||||||
await act(async () => wrapper.find('CloseIcon').simulate('click'));
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.find('AlertModal').length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Label, Tooltip } from '@patternfly/react-core';
|
import { Label, Tooltip } from '@patternfly/react-core';
|
||||||
import { InfoCircleIcon } from '@patternfly/react-icons';
|
import { CheckIcon, InfoCircleIcon } from '@patternfly/react-icons';
|
||||||
import { WorkflowApproval } from '../../../types';
|
import { WorkflowApproval } from '../../../types';
|
||||||
import { formatDateString } from '../../../util/dates';
|
import { formatDateString } from '../../../util/dates';
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ function WorkflowApprovalStatus({ workflowApproval, i18n }) {
|
|||||||
)}
|
)}
|
||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<Label color="red" icon={<InfoCircleIcon />}>
|
<Label variant="outline" color="red" icon={<InfoCircleIcon />}>
|
||||||
{i18n._(t`Denied`)}
|
{i18n._(t`Denied`)}
|
||||||
</Label>
|
</Label>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -52,7 +52,7 @@ function WorkflowApprovalStatus({ workflowApproval, i18n }) {
|
|||||||
)}
|
)}
|
||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<Label color="green" icon={<InfoCircleIcon />}>
|
<Label variant="outline" color="green" icon={<CheckIcon />}>
|
||||||
{i18n._(t`Approved`)}
|
{i18n._(t`Approved`)}
|
||||||
</Label>
|
</Label>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
Reference in New Issue
Block a user