Refactors workflow approval list toolbar and details acttions to add clarity.

This commit is contained in:
Alex Corey
2022-03-24 13:22:12 -04:00
parent e8948a9d6e
commit 4beea35d9e
20 changed files with 813 additions and 569 deletions

View File

@@ -95,7 +95,8 @@
"href",
"modifier",
"data-cy",
"fieldName"
"fieldName",
"splitButtonVariant"
],
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "hh:mm AM/PM", "Twilio"],
"ignoreComponent": [

View File

@@ -6,6 +6,7 @@ import {
ExclamationTriangleIcon,
ClockIcon,
MinusCircleIcon,
InfoCircleIcon,
} from '@patternfly/react-icons';
const Spin = keyframes`
@@ -23,6 +24,8 @@ const RunningIcon = styled(SyncAltIcon)`
RunningIcon.displayName = 'RunningIcon';
const icons = {
approved: CheckCircleIcon,
denied: InfoCircleIcon,
success: CheckCircleIcon,
healthy: CheckCircleIcon,
successful: CheckCircleIcon,

View File

@@ -7,6 +7,8 @@ import { Label, Tooltip } from '@patternfly/react-core';
import icons from '../StatusIcon/icons';
const colors = {
approved: 'green',
denied: 'red',
success: 'green',
successful: 'green',
ok: 'green',
@@ -17,14 +19,17 @@ const colors = {
running: 'blue',
pending: 'blue',
skipped: 'blue',
timedOut: 'red',
waiting: 'grey',
disabled: 'grey',
canceled: 'orange',
changed: 'orange',
};
export default function StatusLabel({ status, tooltipContent = '' }) {
export default function StatusLabel({ status, tooltipContent = '', children }) {
const upperCaseStatus = {
approved: t`Approved`,
denied: t`Denied`,
success: t`Success`,
healthy: t`Healthy`,
successful: t`Successful`,
@@ -35,6 +40,7 @@ export default function StatusLabel({ status, tooltipContent = '' }) {
running: t`Running`,
pending: t`Pending`,
skipped: t`Skipped'`,
timedOut: t`Timed out`,
waiting: t`Waiting`,
disabled: t`Disabled`,
canceled: t`Canceled`,
@@ -46,7 +52,7 @@ export default function StatusLabel({ status, tooltipContent = '' }) {
const renderLabel = () => (
<Label variant="outline" color={color} icon={Icon ? <Icon /> : null}>
{label}
{children || label}
</Label>
);
@@ -65,6 +71,8 @@ export default function StatusLabel({ status, tooltipContent = '' }) {
StatusLabel.propTypes = {
status: oneOf([
'approved',
'denied',
'success',
'successful',
'ok',
@@ -75,6 +83,7 @@ StatusLabel.propTypes = {
'running',
'pending',
'skipped',
'timedOut',
'waiting',
'disabled',
'canceled',

View File

@@ -79,4 +79,11 @@ describe('StatusLabel', () => {
expect(wrapper.find('Tooltip')).toHaveLength(1);
expect(wrapper.find('Tooltip').prop('content')).toEqual('Foo');
});
test('should render children', () => {
const wrapper = mount(
<StatusLabel tooltipContent="Foo" status="success" children="children" />
);
expect(wrapper.text()).toEqual('children');
});
});

View File

@@ -1,21 +1,26 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { t } from '@lingui/macro';
import { Link, useHistory, useParams } from 'react-router-dom';
import { Button } from '@patternfly/react-core';
import AlertModal from 'components/AlertModal';
import { CardBody, CardActionsRow } from 'components/Card';
import DeleteButton from 'components/DeleteButton';
import { Detail, DetailList, UserDateDetail } from 'components/DetailList';
import ErrorDetail from 'components/ErrorDetail';
import { formatDateString, secondsToHHMMSS } from 'util/dates';
import { WorkflowApprovalsAPI } from 'api';
import { WorkflowApprovalsAPI, WorkflowJobsAPI } from 'api';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import { WorkflowApproval } from 'types';
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
import StatusLabel from 'components/StatusLabel';
import {
getDetailPendingLabel,
getStatus,
} from '../shared/WorkflowApprovalUtils';
import WorkflowApprovalControls from '../shared/WorkflowApprovalControls';
function WorkflowApprovalDetail({ workflowApproval }) {
const { id: workflowApprovalId } = useParams();
const [isKebabOpen, setIsKebabModalOpen] = useState(false);
const history = useHistory();
const {
request: deleteWorkflowApproval,
@@ -61,13 +66,36 @@ function WorkflowApprovalDetail({ workflowApproval }) {
const { error: denyError, dismissError: dismissDenyError } =
useDismissableError(denyApprovalError);
const {
error: cancelApprovalError,
isLoading: isCancelLoading,
request: cancelWorkflowApprovals,
} = useRequest(
useCallback(async () => {
await WorkflowJobsAPI.cancel(
workflowApproval.summary_fields.source_workflow_job.id
);
history.push(`/workflow_approvals/${workflowApprovalId}`);
}, [workflowApproval, workflowApprovalId, history]),
{}
);
const handleCancel = async () => {
setIsKebabModalOpen(false);
await cancelWorkflowApprovals();
};
const { error: cancelError, dismissError: dismissCancelError } =
useDismissableError(cancelApprovalError);
const sourceWorkflowJob =
workflowApproval?.summary_fields?.source_workflow_job;
const sourceWorkflowJobTemplate =
workflowApproval?.summary_fields?.workflow_job_template;
const isLoading = isDeleteLoading || isApproveLoading || isDenyLoading;
const isLoading =
isDeleteLoading || isApproveLoading || isDenyLoading || isCancelLoading;
return (
<CardBody>
@@ -86,9 +114,9 @@ function WorkflowApprovalDetail({ workflowApproval }) {
<Detail
label={t`Expires`}
value={
workflowApproval.approval_expiration
? formatDateString(workflowApproval.approval_expiration)
: t`Never`
<StatusLabel status={workflowApproval.status}>
{getDetailPendingLabel(workflowApproval)}
</StatusLabel>
}
dataCy="wa-detail-expires"
/>
@@ -96,9 +124,7 @@ function WorkflowApprovalDetail({ workflowApproval }) {
{workflowApproval.status !== 'pending' && (
<Detail
label={t`Status`}
value={
<WorkflowApprovalStatus workflowApproval={workflowApproval} />
}
value={<StatusLabel status={getStatus(workflowApproval)} />}
dataCy="wa-detail-status"
/>
)}
@@ -169,31 +195,21 @@ function WorkflowApprovalDetail({ workflowApproval }) {
/>
</DetailList>
<CardActionsRow>
{workflowApproval.can_approve_or_deny && (
<>
<Button
ouiaId={`${workflowApproval.id}-approve-button`}
aria-label={t`Approve`}
variant="primary"
onClick={approveWorkflowApproval}
isDisabled={isLoading}
>
{t`Approve`}
</Button>
<Button
ouiaId={`${workflowApproval.id}-deny-button`}
aria-label={t`Deny`}
variant="danger"
onClick={denyWorkflowApproval}
isDisabled={isLoading}
>
{t`Deny`}
</Button>
</>
)}
{workflowApproval.status === 'pending' &&
workflowApproval.can_approve_or_deny && (
<WorkflowApprovalControls
selected={[workflowApproval]}
onHandleApprove={approveWorkflowApproval}
onHandleDeny={denyWorkflowApproval}
onHandleCancel={handleCancel}
onHandleToggleToolbarKebab={(isOpen) =>
setIsKebabModalOpen(isOpen)
}
isKebabOpen={isKebabOpen}
/>
)}
{workflowApproval.status !== 'pending' &&
workflowApproval.summary_fields.user_capabilities &&
workflowApproval.summary_fields.user_capabilities.delete && (
workflowApproval.summary_fields?.user_capabilities?.delete && (
<DeleteButton
name={workflowApproval.name}
modalTitle={t`Delete Workflow Approval`}
@@ -226,6 +242,17 @@ function WorkflowApprovalDetail({ workflowApproval }) {
<ErrorDetail error={approveError} />
</AlertModal>
)}
{cancelError && (
<AlertModal
isOpen={cancelError}
variant="error"
title={t`Error!`}
onClose={dismissCancelError}
>
{t`Failed to approve workflow approval.`}
<ErrorDetail error={cancelError} />
</AlertModal>
)}
{denyError && (
<AlertModal
isOpen={denyError}

View File

@@ -50,8 +50,7 @@ describe('<WorkflowApprovalDetail />', () => {
);
assertDetail('Last Modified', formatDateString(workflowApproval.modified));
assertDetail('Elapsed', '00:00:22');
expect(wrapper.find('Button[aria-label="Approve"]').length).toBe(1);
expect(wrapper.find('Button[aria-label="Deny"]').length).toBe(1);
expect(wrapper.find('WorkflowApprovalControls').length).toBe(1);
});
test('should show expiration date/time', () => {
@@ -126,9 +125,7 @@ describe('<WorkflowApprovalDetail />', () => {
}}
/>
);
expect(wrapper.find('WorkflowApprovalStatus Label').text()).toBe(
'Approved'
);
expect(wrapper.find('StatusLabel').text()).toBe('Approved');
});
test('should show actor when available', () => {
@@ -160,6 +157,20 @@ describe('<WorkflowApprovalDetail />', () => {
);
expect(wrapper.find('WorkflowApprovalActionButtons').length).toBe(0);
});
test('only the delete button should render when approval is not pending', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalDetail
workflowApproval={{
...workflowApproval,
can_approve_or_deny: true,
status: 'successful',
}}
/>
);
expect(wrapper.find('WorkflowApprovalControls').length).toBe(0);
expect(wrapper.find('Button[aria-label="Approve"]').length).toBe(0);
expect(wrapper.find('DeleteButton').length).toBe(1);
});
test('Error dialog shown for failed approval', async () => {
WorkflowApprovalsAPI.approve.mockImplementationOnce(() =>
@@ -168,12 +179,8 @@ describe('<WorkflowApprovalDetail />', () => {
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')();
wrapper.find('DropdownToggleAction').invoke('onClick')();
});
expect(WorkflowApprovalsAPI.approve).toHaveBeenCalledTimes(1);
await waitForElement(
@@ -198,12 +205,16 @@ describe('<WorkflowApprovalDetail />', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalDetail workflowApproval={workflowApproval} />
);
await act(async () => wrapper.find('Toggle').prop('onToggle')(true));
wrapper.update();
await waitForElement(
wrapper,
'WorkflowApprovalDetail Button[aria-label="Deny"]'
'WorkflowApprovalDetail DropdownItem[ouiaId="workflow-deny-button"]'
);
await act(async () => {
wrapper.find('Button[aria-label="Deny"]').invoke('onClick')();
wrapper
.find('DropdownItem[ouiaId="workflow-deny-button"]')
.invoke('onClick')();
});
expect(WorkflowApprovalsAPI.deny).toHaveBeenCalledTimes(1);
await waitForElement(
@@ -244,7 +255,7 @@ describe('<WorkflowApprovalDetail />', () => {
expect(wrapper.find('DeleteButton').length).toBe(0);
});
test('delete button should be hidden when job is pending', () => {
test('delete button should be hidden when job is pending and approve, action buttons should render', async () => {
const wrapper = mountWithContexts(
<WorkflowApprovalDetail
workflowApproval={{
@@ -256,10 +267,44 @@ describe('<WorkflowApprovalDetail />', () => {
start: false,
},
},
can_approve_or_deny: true,
}}
/>
);
expect(wrapper.find('DeleteButton').length).toBe(0);
expect(wrapper.find('WorkflowApprovalControls').length).toBe(1);
await act(async () => wrapper.find('Toggle').prop('onToggle')(true));
wrapper.update();
const denyItem = wrapper.find(
'DropdownItem[ouiaId="workflow-deny-button"]'
);
const cancelItem = wrapper.find(
'DropdownItem[ouiaId="workflow-cancel-button"]'
);
expect(denyItem).toHaveLength(1);
expect(denyItem.prop('description')).toBe(
'This will continue the workflow along failure and always paths.'
);
expect(cancelItem).toHaveLength(1);
expect(cancelItem.prop('description')).toBe(
'This will cancel the workflow and no subsequent nodes will execute.'
);
expect(
wrapper.find('DropdownItem[ouiaId="workflow-delete-button"]')
).toHaveLength(0);
});
test('Delete button is visible and approve action is not', async () => {
const wrapper = mountWithContexts(
<WorkflowApprovalDetail
workflowApproval={mockWorkflowApprovals.results[1]}
/>
);
expect(wrapper.find('DeleteButton').length).toBe(1);
expect(wrapper.find('DeleteButton').prop('isDisabled')).toBe(false);
expect(wrapper.find('WorkflowApprovalControls').length).toBe(0);
});
test('Error dialog shown for failed deletion', async () => {

View File

@@ -1,8 +1,8 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom';
import { t, Plural } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core';
import { WorkflowApprovalsAPI } from 'api';
import { WorkflowApprovalsAPI, WorkflowJobsAPI } from 'api';
import PaginatedTable, {
HeaderRow,
HeaderCell,
@@ -20,8 +20,7 @@ import useSelected from 'hooks/useSelected';
import { getQSConfig, parseQueryString } from 'util/qs';
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
import useWsWorkflowApprovals from './useWsWorkflowApprovals';
import WorkflowApprovalListApproveButton from './WorkflowApprovalListApproveButton';
import WorkflowApprovalListDenyButton from './WorkflowApprovalListDenyButton';
import WorkflowApprovalControls from '../shared/WorkflowApprovalControls';
const QS_CONFIG = getQSConfig('workflow_approvals', {
page: 1,
@@ -32,6 +31,7 @@ const QS_CONFIG = getQSConfig('workflow_approvals', {
function WorkflowApprovalsList() {
const location = useLocation();
const match = useRouteMatch();
const [isKebabOpen, setIsKebabModalOpen] = useState(false);
const {
result: { results, count, relatedSearchableKeys, searchableKeys },
@@ -45,8 +45,12 @@ function WorkflowApprovalsList() {
WorkflowApprovalsAPI.read(params),
WorkflowApprovalsAPI.readOptions(),
]);
const dataWithModifiedName = response.data.results.map((i) => {
i.name = `${i.summary_fields.source_workflow_job.id} - ${i.name}`;
return i;
});
return {
results: response.data.results,
results: dataWithModifiedName,
count: response.data.count,
relatedSearchableKeys: (
actionsResponse?.data?.related_search_fields || []
@@ -141,12 +145,47 @@ function WorkflowApprovalsList() {
);
const handleDeny = async () => {
setIsKebabModalOpen(false);
await denyWorkflowApprovals();
clearSelected();
};
const { error: actionError, dismissError: dismissActionError } =
useDismissableError(approveApprovalError || denyApprovalError);
const {
error: cancelApprovalError,
isLoading: isCancelLoading,
request: cancelWorkflowApprovals,
} = useRequest(
useCallback(
async () =>
Promise.all(
selected.map(({ summary_fields }) =>
WorkflowJobsAPI.cancel(summary_fields.source_workflow_job.id)
)
),
[selected]
),
{}
);
const handleCancel = async () => {
setIsKebabModalOpen(false);
await cancelWorkflowApprovals();
clearSelected();
};
const { error: approveError, dismissError: dismissApproveError } =
useDismissableError(approveApprovalError);
const { error: cancelError, dismissError: dismissCancelError } =
useDismissableError(cancelApprovalError);
const { error: denyError, dismissError: dismissDenyError } =
useDismissableError(denyApprovalError);
const isLoading =
isWorkflowApprovalsLoading ||
isDeleteLoading ||
isApproveLoading ||
isDenyLoading ||
isCancelLoading;
return (
<>
@@ -154,12 +193,7 @@ function WorkflowApprovalsList() {
<Card>
<PaginatedTable
contentError={contentError}
hasContentLoading={
isWorkflowApprovalsLoading ||
isDeleteLoading ||
isApproveLoading ||
isDenyLoading
}
hasContentLoading={isLoading}
items={workflowApprovals}
itemCount={count}
pluralizedItemName={t`Workflow Approvals`}
@@ -185,15 +219,16 @@ function WorkflowApprovalsList() {
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
additionalControls={[
<WorkflowApprovalListApproveButton
key="approve"
onApprove={handleApprove}
selectedItems={selected}
/>,
<WorkflowApprovalListDenyButton
key="deny"
onDeny={handleDeny}
selectedItems={selected}
<WorkflowApprovalControls
key="approvalControls"
onHandleApprove={handleApprove}
selected={selected}
onHandleDeny={handleDeny}
onHandleCancel={handleCancel}
onHandleToggleToolbarKebab={(isOpen) =>
setIsKebabModalOpen(isOpen)
}
isKebabOpen={isKebabOpen}
/>,
<ToolbarDeleteButton
key="delete"
@@ -218,7 +253,7 @@ function WorkflowApprovalsList() {
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Job`}</HeaderCell>
<HeaderCell>{t`Workflow Job`}</HeaderCell>
<HeaderCell sortKey="started">{t`Started`}</HeaderCell>
<HeaderCell>{t`Status`}</HeaderCell>
</HeaderRow>
@@ -250,17 +285,37 @@ function WorkflowApprovalsList() {
<ErrorDetail error={deletionError} />
</AlertModal>
)}
{actionError && (
{approveError && (
<AlertModal
isOpen={actionError}
isOpen={approveError}
variant="error"
title={t`Error!`}
onClose={dismissActionError}
onClose={dismissApproveError}
>
{approveApprovalError
? t`Failed to approve one or more workflow approval.`
: t`Failed to deny one or more workflow approval.`}
<ErrorDetail error={actionError} />
{t`Failed to approve one or more workflow approval.`}
<ErrorDetail error={approveError} />
</AlertModal>
)}
{cancelError && (
<AlertModal
isOpen={cancelError}
variant="error"
title={t`Error!`}
onClose={dismissCancelError}
>
{t`Failed to cancel one or more workflow approval.`}
<ErrorDetail error={cancelError} />
</AlertModal>
)}
{denyError && (
<AlertModal
isOpen={denyError}
variant="error"
title={t`Error!`}
onClose={dismissDenyError}
>
{t`Failed to deny one or more workflow approval.`}
<ErrorDetail error={denyError} />
</AlertModal>
)}
</>

View File

@@ -38,7 +38,7 @@ describe('<WorkflowApprovalList />', () => {
});
wrapper.update();
expect(wrapper.find('WorkflowApprovalListItem')).toHaveLength(3);
expect(wrapper.find('WorkflowApprovalListItem')).toHaveLength(4);
});
test('should select workflow approval when checked', async () => {
@@ -69,7 +69,7 @@ describe('<WorkflowApprovalList />', () => {
wrapper.update();
const items = wrapper.find('WorkflowApprovalListItem');
expect(items).toHaveLength(3);
expect(items).toHaveLength(4);
items.forEach((item) => {
expect(item.prop('isSelected')).toEqual(true);
});
@@ -79,19 +79,64 @@ describe('<WorkflowApprovalList />', () => {
).toEqual(true);
});
test('should disable delete button', async () => {
test('WorkflowapprovalControls is inactive. Delete button is active', async () => {
await act(async () => {
wrapper = mountWithContexts(<WorkflowApprovalList />);
});
wrapper.update();
await act(async () => {
wrapper.find('WorkflowApprovalListItem').first().invoke('onSelect')();
});
wrapper.update();
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
true
);
expect(
wrapper
.find('WorkflowApprovalControls')
.find('DropdownToggle')
.prop('isDisabled')
).toBe(false);
});
test('WorkflowapprovalControls is inactive. Delete button is active', async () => {
await act(async () => {
wrapper = mountWithContexts(<WorkflowApprovalList />);
});
wrapper.update();
await act(async () => {
wrapper.find('WorkflowApprovalListItem').at(1).invoke('onSelect')();
});
wrapper.update();
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
false
);
expect(
wrapper
.find('WorkflowApprovalControls')
.find('DropdownToggle')
.prop('isDisabled')
).toBe(true);
});
test('should disable WorkflowApprovalControls toggle with active delete button', async () => {
await act(async () => {
wrapper = mountWithContexts(<WorkflowApprovalList />);
});
wrapper.update();
await act(async () => {
wrapper.find('WorkflowApprovalListItem').at(2).invoke('onSelect')();
wrapper.find('WorkflowApprovalListItem').at(3).invoke('onSelect')();
});
wrapper.update();
expect(wrapper.find('ToolbarDeleteButton button').prop('disabled')).toEqual(
true
expect(
wrapper
.find('WorkflowApprovalControls')
.find('DropdownToggle')
.prop('isDisabled')
).toEqual(true);
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
false
);
});
@@ -105,10 +150,17 @@ describe('<WorkflowApprovalList />', () => {
wrapper.find('WorkflowApprovalListItem').at(1).invoke('onSelect')();
});
wrapper.update();
await act(async () => {
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
});
expect(
wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')
).toEqual(false);
await act(async () =>
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
);
wrapper.update();
await act(async () =>
wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')()
);
expect(WorkflowApprovalsAPI.destroy).toHaveBeenCalledTimes(1);
});
@@ -129,14 +181,22 @@ describe('<WorkflowApprovalList />', () => {
});
wrapper.update();
expect(WorkflowApprovalsAPI.read).toHaveBeenCalledTimes(1);
await act(async () => {
wrapper.find('WorkflowApprovalListItem').at(1).invoke('onSelect')();
});
wrapper.update();
expect(
wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')
).toEqual(false);
await act(async () =>
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
);
await act(async () => {
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
});
wrapper.update();
await act(async () =>
wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')()
);
wrapper.update();
const modal = wrapper.find('Modal');

View File

@@ -1,77 +0,0 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useContext } from '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 }) {
const { isKebabified } = useContext(KebabifiedContext);
const renderTooltip = () => {
if (selectedItems.length === 0) {
return t`Select a row to approve`;
}
const itemsUnableToApprove = selectedItems
.filter(cannotApprove)
.map((item) => item.name)
.join(', ');
if (selectedItems.some(cannotApprove)) {
return t`You are unable to act on the following workflow approvals: ${itemsUnableToApprove}`;
}
return t`Approve`;
};
const isDisabled =
selectedItems.length === 0 || selectedItems.some(cannotApprove);
return (
<>
{isKebabified ? (
<DropdownItem
key="approve"
isDisabled={isDisabled}
component="button"
onClick={onApprove}
ouiaId="workflow-approval-approve-dropdown-item"
>
{t`Approve`}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
ouiaId="workflow-approval-approve-button"
isDisabled={isDisabled}
aria-label={t`Approve`}
variant="primary"
onClick={onApprove}
>
{t`Approve`}
</Button>
</div>
</Tooltip>
)}
</>
);
}
WorkflowApprovalListApproveButton.propTypes = {
onApprove: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(WorkflowApproval),
};
WorkflowApprovalListApproveButton.defaultProps = {
selectedItems: [],
};
export default WorkflowApprovalListApproveButton;

View File

@@ -1,56 +0,0 @@
import React from 'react';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import WorkflowApprovalListApproveButton from './WorkflowApprovalListApproveButton';
const workflowApproval = {
id: 1,
name: 'Foo',
can_approve_or_deny: true,
url: '/api/v2/workflow_approvals/218/',
};
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');
});
});

View File

@@ -1,77 +0,0 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useContext } from '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 }) {
const { isKebabified } = useContext(KebabifiedContext);
const renderTooltip = () => {
if (selectedItems.length === 0) {
return t`Select a row to deny`;
}
const itemsUnableToDeny = selectedItems
.filter(cannotDeny)
.map((item) => item.name)
.join(', ');
if (selectedItems.some(cannotDeny)) {
return t`You are unable to act on the following workflow approvals: ${itemsUnableToDeny}`;
}
return t`Deny`;
};
const isDisabled =
selectedItems.length === 0 || selectedItems.some(cannotDeny);
return (
<>
{isKebabified ? (
<DropdownItem
key="deny"
isDisabled={isDisabled}
component="button"
onClick={onDeny}
ouiaId="workflow-approval-deny-dropdown-item"
>
{t`Deny`}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
ouiaId="workflow-approval-deny-button"
isDisabled={isDisabled}
aria-label={t`Deny`}
variant="danger"
onClick={onDeny}
>
{t`Deny`}
</Button>
</div>
</Tooltip>
)}
</>
);
}
WorkflowApprovalListDenyButton.propTypes = {
onDeny: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(WorkflowApproval),
};
WorkflowApprovalListDenyButton.defaultProps = {
selectedItems: [],
};
export default WorkflowApprovalListDenyButton;

View File

@@ -1,53 +0,0 @@
import React from 'react';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import WorkflowApprovalListDenyButton from './WorkflowApprovalListDenyButton';
const workflowApproval = {
id: 1,
name: 'Foo',
can_approve_or_deny: true,
url: '/api/v2/workflow_approvals/218/',
};
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');
});
});

View File

@@ -1,18 +1,16 @@
import React from 'react';
import { t } from '@lingui/macro';
import { string, bool, func } from 'prop-types';
import { Label } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { WorkflowApproval } from 'types';
import { formatDateString } from 'util/dates';
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
const JobLabel = styled.b`
margin-right: 24px;
`;
import StatusLabel from 'components/StatusLabel';
import {
getPendingLabel,
getStatus,
getTooltip,
} from '../shared/WorkflowApprovalUtils';
function WorkflowApprovalListItem({
workflowApproval,
@@ -24,28 +22,6 @@ function WorkflowApprovalListItem({
const labelId = `check-action-${workflowApproval.id}`;
const workflowJob = workflowApproval?.summary_fields?.source_workflow_job;
const getStatus = () => {
if (
workflowApproval.status === 'pending' &&
workflowApproval.approval_expiration
) {
return (
<Label>
{t`Expires on ${formatDateString(
workflowApproval.approval_expiration
)}`}
</Label>
);
}
if (
workflowApproval.status === 'pending' &&
!workflowApproval.approval_expiration
) {
return <Label>{t`Never expires`}</Label>;
}
return <WorkflowApprovalStatus workflowApproval={workflowApproval} />;
};
return (
<Tr id={`workflow-approval-row-${workflowApproval.id}`}>
<Td
@@ -62,22 +38,28 @@ function WorkflowApprovalListItem({
</Link>
</Td>
<Td>
<>
<JobLabel>{t`Job`}</JobLabel>
{workflowJob && workflowJob?.id ? (
<Link to={`/jobs/workflow/${workflowJob?.id}`}>
{`${workflowJob?.id} - ${workflowJob?.name}`}
</Link>
) : (
t`Deleted`
)}
</>
{workflowJob && workflowJob?.id ? (
<Link to={`/jobs/workflow/${workflowJob?.id}`}>
{`${workflowJob?.id} - ${workflowJob?.name}`}
</Link>
) : (
t`Deleted`
)}
</Td>
<Td dataLabel={t`Started`}>
{formatDateString(workflowApproval.started)}
</Td>
<Td dataLabel={t`Status`}>
<div>{getStatus()}</div>
{workflowApproval.status === 'pending' ? (
<StatusLabel status={workflowApproval.status}>
{getPendingLabel(workflowApproval)}
</StatusLabel>
) : (
<StatusLabel
tooltipContent={getTooltip(workflowApproval)}
status={getStatus(workflowApproval)}
/>
)}
</Td>
</Tr>
);

View File

@@ -222,6 +222,77 @@
"can_approve_or_deny": false,
"approval_expiration": null,
"timed_out": false
},
{
"id": 220,
"type": "workflow_approval",
"url": "/api/v2/workflow_approvals/218/",
"related": {
"created_by": "/api/v2/users/1/",
"unified_job_template": "/api/v2/workflow_approval_templates/10/",
"source_workflow_job": "/api/v2/workflow_jobs/216/",
"workflow_approval_template": "/api/v2/workflow_approval_templates/10/",
"approve": "/api/v2/workflow_approvals/218/approve/",
"deny": "/api/v2/workflow_approvals/218/deny/"
},
"summary_fields": {
"workflow_job_template": {
"id": 9,
"name": "Approval @ 9:15:26 AM",
"description": ""
},
"workflow_job": {
"id": 216,
"name": "Approval @ 9:15:26 AM",
"description": ""
},
"workflow_approval_template": {
"id": 10,
"name": "approval copy",
"description": "",
"timeout": 0
},
"unified_job_template": {
"id": 10,
"name": "approval copy",
"description": "",
"unified_job_type": "workflow_approval"
},
"created_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"user_capabilities": {
"delete": true,
"start": true
},
"source_workflow_job": {
"id": 216,
"name": "Approval @ 9:15:26 AM",
"description": "",
"status": "running",
"failed": false,
"elapsed": 0.0
}
},
"created": "2020-10-09T17:13:12.067947Z",
"modified": "2020-10-09T17:13:12.068147Z",
"name": "approval",
"description": "description of approval",
"unified_job_template": 10,
"launch_type": "workflow",
"status": "successful",
"failed": false,
"started": "2020-10-09T17:13:12.067947Z",
"finished": null,
"canceled_on": null,
"elapsed": 22.879029,
"job_explanation": "",
"can_approve_or_deny": true,
"approval_expiration": null,
"timed_out": false
}
]
}

View File

@@ -0,0 +1,146 @@
import React from 'react';
import { t } from '@lingui/macro';
import {
Dropdown as PFDropdown,
DropdownItem,
Tooltip,
DropdownToggle,
DropdownToggleAction,
} from '@patternfly/react-core';
import { CaretDownIcon } from '@patternfly/react-icons';
import { useKebabifiedMenu } from 'contexts/Kebabified';
import styled from 'styled-components';
const Dropdown = styled(PFDropdown)`
--pf-c-dropdown__toggle--disabled--BackgroundColor: var(
--pf-global--disabled-color--200
);
&&& {
button {
color: var(--pf-c-dropdown__toggle--m-primary--Color);
}
div.pf-m-disabled > button {
color: var(--pf-global--disabled-color--100);
}
}
`;
function isPending(item) {
return item.status === 'pending';
}
function isComplete(item) {
return item.status !== 'pending';
}
function WorkflowApprovalControls({
selected,
onHandleDeny,
onHandleCancel,
onHandleApprove,
onHandleToggleToolbarKebab,
isKebabOpen,
}) {
const { isKebabified } = useKebabifiedMenu();
const hasSelectedItems = selected.length > 0;
const isApproveDenyOrCancelDisabled =
!hasSelectedItems ||
!selected.every(
(item) => item.status === 'pending' && item.can_approve_or_deny
);
const renderTooltip = (action) => {
if (!hasSelectedItems) {
return t`Select items to approve, deny, or cancel`;
}
if (!selected.some(isPending)) {
return t`Cannot approve, cancel or deny completed workflow approvals`;
}
const completedItems = selected
.filter(isComplete)
.map((item) => item.name)
.join(', ');
if (selected.some(isPending) && selected.some(isComplete)) {
return t`The following selected items are complete and cannot be acted on: ${completedItems}`;
}
return action;
};
const dropdownItems = [
<Tooltip key="deny" content={renderTooltip(t`Deny`)}>
<div>
<DropdownItem
isDisabled={isApproveDenyOrCancelDisabled}
onClick={onHandleDeny}
ouiaId="workflow-deny-button"
description={t`This will continue the workflow along failure and always paths.`}
>
{t`Deny`}
</DropdownItem>
</div>
</Tooltip>,
<Tooltip key="cancel" content={renderTooltip(t`Cancel`)}>
<div>
<DropdownItem
isDisabled={isApproveDenyOrCancelDisabled}
ouiaId="workflow-cancel-button"
onClick={onHandleCancel}
description={t`This will cancel the workflow and no subsequent nodes will execute.`}
>
{t`Cancel`}
</DropdownItem>
</div>
</Tooltip>,
];
if (isKebabified) {
dropdownItems.unshift(
<Tooltip content={renderTooltip(t`Approve`)}>
<div>
<DropdownItem
isDisabled={isApproveDenyOrCancelDisabled}
onClick={onHandleApprove}
ouiaId="workflow-approve-button"
description={t`This will continue the workflow`}
>{t`Approve`}</DropdownItem>
</div>
</Tooltip>
);
return dropdownItems;
}
return (
<Tooltip
content={renderTooltip(t`Approve, cancel or deny`)}
key="workflowApproveOrDenyControls"
>
<Dropdown
toggle={
<DropdownToggle
isPrimary
splitButtonVariant="action"
toggleIndicator={CaretDownIcon}
splitButtonItems={[
<DropdownToggleAction
key="action"
type="button"
onClick={onHandleApprove}
>
{t`Approve`}
</DropdownToggleAction>,
]}
data-cy="actions-kebab-toogle"
isDisabled={isApproveDenyOrCancelDisabled}
onToggle={onHandleToggleToolbarKebab}
/>
}
isOpen={isKebabOpen}
isPlain
ouiaId="actions-dropdown"
dropdownItems={dropdownItems}
/>
</Tooltip>
);
}
export default WorkflowApprovalControls;

View File

@@ -0,0 +1,94 @@
import React from 'react';
import {
mountWithContexts,
shallowWithContexts,
} from '../../../../testUtils/enzymeHelpers';
import WorkflowApprovalControls from './WorkflowApprovalControls';
import mockData from '../data.workflowApprovals.json';
describe('<WorkflowApprovalControls/> shallow mount', () => {
let wrapper;
let onHandleDeny = jest.fn();
let onHandleCancel = jest.fn();
let onHandleToggleToolbarKebab = jest.fn();
let mockApprovalList = mockData.results;
test('initially render successfully', () => {
wrapper = shallowWithContexts(
<WorkflowApprovalControls
selected={[]}
onHandleDeny={onHandleDeny}
onHandleCancel={onHandleCancel}
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
isKebabOpen={false}
/>
);
expect(wrapper.find('WorkflowApprovalControls')).toHaveLength(1);
expect(wrapper.find('WorkflowApprovalControls').prop('isKebabOpen')).toBe(
false
);
});
test('is open with the correct dropdown options', async () => {
wrapper = shallowWithContexts(
<WorkflowApprovalControls
selected={mockApprovalList}
onHandleDeny={onHandleDeny}
onHandleCancel={onHandleCancel}
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
isKebabOpen={true}
/>
);
expect(wrapper.find('WorkflowApprovalControls')).toHaveLength(1);
expect(wrapper.find('WorkflowApprovalControls').prop('isKebabOpen')).toBe(
true
);
});
});
describe('<WorkflowApprovalControls/>, full mount', () => {
let wrapper;
let onHandleDeny = jest.fn();
let onHandleCancel = jest.fn();
let onHandleToggleToolbarKebab = jest.fn();
let mockApprovalList = mockData.results;
const cancelButton = 'DropdownItem[ouiaId="workflow-cancel-button"]';
const denyButton = 'DropdownItem[ouiaId="workflow-deny-button"]';
test('initially render successfully', () => {
wrapper = mountWithContexts(
<WorkflowApprovalControls
selected={[]}
onHandleDeny={onHandleDeny}
onHandleCancel={onHandleCancel}
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
isKebabOpen={false}
/>
);
expect(wrapper.find('WorkflowApprovalControls')).toHaveLength(1);
expect(wrapper.find('WorkflowApprovalControls').prop('isKebabOpen')).toBe(
false
);
expect(wrapper.find('Tooltip').prop('content')).toBe(
'Select items to approve, deny, or cancel'
);
});
test('should call correct functions', () => {
wrapper = mountWithContexts(
<WorkflowApprovalControls
selected={[mockApprovalList[1], mockApprovalList[2]]}
onHandleDeny={onHandleDeny}
onHandleCancel={onHandleCancel}
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
isKebabOpen={true}
/>
);
wrapper.find(denyButton).prop('onClick')();
expect(onHandleDeny).toHaveBeenCalled();
wrapper.find(cancelButton).prop('onClick')();
expect(onHandleCancel).toHaveBeenCalled();
wrapper.find('DropdownToggle').prop('onToggle')(false);
expect(onHandleToggleToolbarKebab).toHaveBeenCalledWith(false);
});
});

View File

@@ -1,72 +0,0 @@
import React from 'react';
import { t } from '@lingui/macro';
import { Label, Tooltip } from '@patternfly/react-core';
import { CheckIcon, InfoCircleIcon } from '@patternfly/react-icons';
import { WorkflowApproval } from 'types';
import { formatDateString } from 'util/dates';
function WorkflowApprovalStatus({ workflowApproval }) {
if (workflowApproval.status === 'pending') {
return workflowApproval.approval_expiration
? t`Expires on ${formatDateString(workflowApproval.approval_expiration)}`
: t`Never expires`;
}
if (workflowApproval.timed_out) {
return <Label color="red">{t`Timed out`}</Label>;
}
if (workflowApproval.canceled_on) {
return <Label color="red">{t`Canceled`}</Label>;
}
if (workflowApproval.status === 'failed' && workflowApproval.failed) {
return (
<Tooltip
content={
workflowApproval.summary_fields?.approved_or_denied_by?.username
? t`Denied by ${
workflowApproval.summary_fields.approved_or_denied_by.username
} - ${formatDateString(workflowApproval.finished)}`
: t`Denied - ${formatDateString(
workflowApproval.finished
)}. See the Activity Stream for more information.`
}
position="top"
>
<Label variant="outline" color="red" icon={<InfoCircleIcon />}>
{t`Denied`}
</Label>
</Tooltip>
);
}
if (workflowApproval.status === 'successful') {
return (
<Tooltip
content={
workflowApproval.summary_fields?.approved_or_denied_by?.username
? t`Approved by ${
workflowApproval.summary_fields.approved_or_denied_by.username
} - ${formatDateString(workflowApproval.finished)}`
: t`Approved - ${formatDateString(
workflowApproval.finished
)}. See the Activity Stream for more information.`
}
position="top"
>
<Label variant="outline" color="green" icon={<CheckIcon />}>
{t`Approved`}
</Label>
</Tooltip>
);
}
return null;
}
WorkflowApprovalStatus.defaultProps = {
workflowApproval: WorkflowApproval.isRequired,
};
export default WorkflowApprovalStatus;

View File

@@ -1,99 +0,0 @@
import React from 'react';
import { formatDateString } from 'util/dates';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import WorkflowApprovalStatus from './WorkflowApprovalStatus';
import mockWorkflowApprovals from '../data.workflowApprovals.json';
const workflowApproval = mockWorkflowApprovals.results[0];
describe('<WorkflowApprovalStatus />', () => {
let wrapper;
test('shows no expiration when approval status is pending and no approval_expiration', () => {
wrapper = mountWithContexts(
<WorkflowApprovalStatus workflowApproval={workflowApproval} />
);
expect(wrapper.text()).toBe('Never expires');
});
test('shows expiration date/time when approval status is pending and approval_expiration present', () => {
wrapper = mountWithContexts(
<WorkflowApprovalStatus
workflowApproval={{
...workflowApproval,
approval_expiration: '2020-10-10T17:13:12.067947Z',
}}
/>
);
expect(wrapper.text()).toBe(
`Expires on ${formatDateString('2020-10-10T17:13:12.067947Z')}`
);
});
test('shows when an approval has timed out', () => {
wrapper = mountWithContexts(
<WorkflowApprovalStatus
workflowApproval={{
...workflowApproval,
status: 'failed',
timed_out: true,
}}
/>
);
expect(wrapper.find('Label').text()).toBe('Timed out');
});
test('shows when an approval has canceled', () => {
wrapper = mountWithContexts(
<WorkflowApprovalStatus
workflowApproval={{
...workflowApproval,
status: 'canceled',
canceled_on: '2020-10-10T17:13:12.067947Z',
}}
/>
);
expect(wrapper.find('Label').text()).toBe('Canceled');
});
test('shows when an approval has approved', () => {
wrapper = mountWithContexts(
<WorkflowApprovalStatus
workflowApproval={{
...workflowApproval,
summary_fields: {
...workflowApproval.summary_fields,
approved_or_denied_by: {
id: 1,
username: 'Foobar',
},
},
status: 'successful',
finished: '2020-10-10T17:13:12.067947Z',
}}
/>
);
expect(wrapper.find('Label').text()).toBe('Approved');
});
test('shows when an approval has denied', () => {
wrapper = mountWithContexts(
<WorkflowApprovalStatus
workflowApproval={{
...workflowApproval,
summary_fields: {
...workflowApproval.summary_fields,
approved_or_denied_by: {
id: 1,
username: 'Foobar',
},
},
status: 'failed',
finished: '2020-10-10T17:13:12.067947Z',
failed: true,
}}
/>
);
expect(wrapper.find('Label').text()).toBe('Denied');
});
});

View File

@@ -0,0 +1,61 @@
import { t } from '@lingui/macro';
import { formatDateString } from 'util/dates';
export function getTooltip(workflowApproval) {
if (workflowApproval.status === 'successful') {
if (workflowApproval.summary_fields?.approved_or_denied_by?.username) {
return t`Approved by ${
workflowApproval.summary_fields.approved_or_denied_by.username
} - ${formatDateString(workflowApproval.finished)}`;
}
return t`Approved - ${formatDateString(
workflowApproval.finished
)}. See the Activity Stream for more information.`;
}
if (workflowApproval.status === 'failed' && workflowApproval.failed) {
if (workflowApproval.summary_fields?.approved_or_denied_by?.username) {
return t`Denied by ${
workflowApproval.summary_fields.approved_or_denied_by.username
} - ${formatDateString(workflowApproval.finished)}`;
}
return t`Denied - ${formatDateString(
workflowApproval.finished
)}. See the Activity Stream for more information.`;
}
return '';
}
export function getStatus(workflowApproval) {
if (workflowApproval.timed_out) {
return 'timedOut';
}
if (workflowApproval.canceled_on) {
return 'canceled';
}
if (workflowApproval.status === 'failed' && workflowApproval.failed) {
return 'denied';
}
if (workflowApproval.status === 'successful') {
return 'approved';
}
return workflowApproval.status;
}
export function getPendingLabel(workflowApproval) {
if (!workflowApproval.approval_expiration) {
return t`Never expires`;
}
return t`Expires on ${formatDateString(
workflowApproval.approval_expiration
)}`;
}
export function getDetailPendingLabel(workflowApproval) {
if (!workflowApproval.approval_expiration) {
return t`Never`;
}
return `${formatDateString(workflowApproval.approval_expiration)}`;
}

View File

@@ -0,0 +1,117 @@
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
import { formatDateString } from 'util/dates';
import {
getPendingLabel,
getStatus,
getTooltip,
} from '../shared/WorkflowApprovalUtils';
import mockWorkflowApprovals from '../data.workflowApprovals.json';
const workflowApproval = mockWorkflowApprovals.results[0];
i18n.loadLocaleData('en', { plurals: en });
async function activate() {
const { messages } = await import(`../../../locales/${'en'}/messages.js`);
i18n.load('en', messages);
i18n.activate('en');
}
activate();
describe('<WorkflowApproval />', () => {
test('shows no expiration when approval status is pending and no approval_expiration', () => {
expect(getPendingLabel(workflowApproval)).toEqual('Never expires');
});
test('shows expiration date/time when approval status is pending and approval_expiration present', () => {
workflowApproval.approval_expiration = '2020-10-10T17:13:12.067947Z';
expect(getPendingLabel(workflowApproval)).toEqual(
`Expires on 10/10/2020, 5:13:12 PM`
);
});
test('shows when an approval has timed out', () => {
workflowApproval.status = 'failed';
workflowApproval.timed_out = true;
expect(getStatus(workflowApproval)).toEqual('timedOut');
});
test('shows when an approval has canceled', () => {
workflowApproval.status = 'canceled';
workflowApproval.canceled_on = '2020-10-10T17:13:12.067947Z';
workflowApproval.timed_out = false;
expect(getStatus(workflowApproval)).toEqual('canceled');
});
test('shows when an approval has beeen approved', () => {
workflowApproval.summary_fields = {
...workflowApproval.summary_fields,
approved_or_denied_by: { id: 1, username: 'Foobar' },
};
workflowApproval.status = 'successful';
workflowApproval.canceled_on = '';
workflowApproval.finished = '';
expect(getStatus(workflowApproval)).toEqual('approved');
});
test('shows when an approval has timed out', () => {
workflowApproval.summary_fields = {
...workflowApproval.summary_fields,
approved_or_denied_by: { id: 1, username: 'Foobar' },
};
workflowApproval.status = 'failed';
workflowApproval.finished = '';
workflowApproval.failed = true;
expect(getStatus(workflowApproval)).toEqual('denied');
});
test('shows correct approved tooltip with user', () => {
workflowApproval.summary_fields = {
...workflowApproval.summary_fields,
approved_or_denied_by: { id: 1, username: 'Foobar' },
};
workflowApproval.status = 'successful';
workflowApproval.finished = '2020-10-10T17:13:12.067947Z';
expect(getTooltip(workflowApproval)).toEqual(
'Approved by Foobar - 10/10/2020, 5:13:12 PM'
);
});
test('shows correct approved tooltip without user', () => {
workflowApproval.summary_fields = {
...workflowApproval.summary_fields,
approved_or_denied_by: {},
};
workflowApproval.status = 'successful';
workflowApproval.finished = '2020-10-10T17:13:12.067947Z';
expect(getTooltip(workflowApproval)).toEqual(
'Approved - 10/10/2020, 5:13:12 PM. See the Activity Stream for more information.'
);
});
test('shows correct denial tooltip with user', () => {
workflowApproval.summary_fields = {
...workflowApproval.summary_fields,
approved_or_denied_by: { id: 1, username: 'Foobar' },
};
workflowApproval.status = 'failed';
workflowApproval.finished = '2020-10-10T17:13:12.067947Z';
workflowApproval.failed = true;
expect(getTooltip(workflowApproval)).toEqual(
'Denied by Foobar - 10/10/2020, 5:13:12 PM'
);
});
test('shows correct denial tooltip without user', () => {
workflowApproval.summary_fields = {
...workflowApproval.summary_fields,
approved_or_denied_by: {},
};
workflowApproval.status = 'failed';
workflowApproval.finished = '2020-10-10T17:13:12.067947Z';
workflowApproval.failed = true;
expect(getTooltip(workflowApproval)).toEqual(
'Denied - 10/10/2020, 5:13:12 PM. See the Activity Stream for more information.'
);
});
});