mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 07:26:03 -03:30
Re-add workflow approval bulk actions to workflow approvals list
This commit is contained in:
@@ -12,11 +12,16 @@ import PaginatedTable, {
|
|||||||
import AlertModal from 'components/AlertModal';
|
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 useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, {
|
||||||
|
useDeleteItems,
|
||||||
|
useDismissableError,
|
||||||
|
} from 'hooks/useRequest';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
||||||
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,
|
||||||
@@ -104,7 +109,50 @@ function WorkflowApprovalsList() {
|
|||||||
clearSelected();
|
clearSelected();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading = isWorkflowApprovalsLoading || isDeleteLoading;
|
const {
|
||||||
|
error: approveApprovalError,
|
||||||
|
isLoading: isApproveLoading,
|
||||||
|
request: approveWorkflowApprovals,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(
|
||||||
|
async () =>
|
||||||
|
Promise.all(selected.map(({ id }) => WorkflowApprovalsAPI.approve(id))),
|
||||||
|
[selected]
|
||||||
|
),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
await approveWorkflowApprovals();
|
||||||
|
clearSelected();
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: denyApprovalError,
|
||||||
|
isLoading: isDenyLoading,
|
||||||
|
request: denyWorkflowApprovals,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(
|
||||||
|
async () =>
|
||||||
|
Promise.all(selected.map(({ id }) => WorkflowApprovalsAPI.deny(id))),
|
||||||
|
[selected]
|
||||||
|
),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeny = async () => {
|
||||||
|
await denyWorkflowApprovals();
|
||||||
|
clearSelected();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error: actionError, dismissError: dismissActionError } =
|
||||||
|
useDismissableError(approveApprovalError || denyApprovalError);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isWorkflowApprovalsLoading ||
|
||||||
|
isDeleteLoading ||
|
||||||
|
isApproveLoading ||
|
||||||
|
isDenyLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -138,6 +186,16 @@ function WorkflowApprovalsList() {
|
|||||||
onSelectAll={selectAll}
|
onSelectAll={selectAll}
|
||||||
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}
|
||||||
@@ -193,6 +251,19 @@ function WorkflowApprovalsList() {
|
|||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
|
{actionError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={actionError}
|
||||||
|
variant="error"
|
||||||
|
title={t`Error!`}
|
||||||
|
onClose={dismissActionError}
|
||||||
|
>
|
||||||
|
{approveApprovalError
|
||||||
|
? t`Failed to approve one or more workflow approval.`
|
||||||
|
: t`Failed to deny one or more workflow approval.`}
|
||||||
|
<ErrorDetail error={actionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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 (
|
||||||
|
/* eslint-disable-next-line react/jsx-no-useless-fragment */
|
||||||
|
<>
|
||||||
|
{isKebabified ? (
|
||||||
|
<DropdownItem
|
||||||
|
key="approve"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
component="button"
|
||||||
|
onClick={onApprove}
|
||||||
|
>
|
||||||
|
{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;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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 (
|
||||||
|
/* eslint-disable-next-line react/jsx-no-useless-fragment */
|
||||||
|
<>
|
||||||
|
{isKebabified ? (
|
||||||
|
<DropdownItem
|
||||||
|
key="deny"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
component="button"
|
||||||
|
onClick={onDeny}
|
||||||
|
>
|
||||||
|
{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;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user