mirror of
https://github.com/ansible/awx.git
synced 2026-02-20 20:50:06 -03:30
Merge pull request #7724 from AlexSCorey/7434-UserTokensDelete
Adds delete functionality to user tokens list Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -5,11 +5,14 @@ import { t } from '@lingui/macro';
|
|||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarAddButton,
|
ToolbarAddButton,
|
||||||
|
ToolbarDeleteButton,
|
||||||
} from '../../../components/PaginatedDataList';
|
} from '../../../components/PaginatedDataList';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||||
import { UsersAPI } from '../../../api';
|
import { UsersAPI, TokensAPI } from '../../../api';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import UserTokensListItem from './UserTokenListItem';
|
import UserTokensListItem from './UserTokenListItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('user', {
|
const QS_CONFIG = getQSConfig('user', {
|
||||||
@@ -54,84 +57,127 @@ function UserTokenList({ i18n }) {
|
|||||||
tokens
|
tokens
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
deleteItems: deleteTokens,
|
||||||
|
deletionError,
|
||||||
|
clearDeletionError,
|
||||||
|
} = useDeleteItems(
|
||||||
|
useCallback(async () => {
|
||||||
|
return Promise.all(
|
||||||
|
selected.map(({ id: tokenId }) => TokensAPI.destroy(tokenId))
|
||||||
|
);
|
||||||
|
}, [selected]),
|
||||||
|
{
|
||||||
|
qsConfig: QS_CONFIG,
|
||||||
|
allItemsSelected: isAllSelected,
|
||||||
|
fetchItems: fetchTokens,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await deleteTokens();
|
||||||
|
setSelected([]);
|
||||||
|
};
|
||||||
|
|
||||||
const canAdd = true;
|
const canAdd = true;
|
||||||
return (
|
return (
|
||||||
<PaginatedDataList
|
<>
|
||||||
contentError={error}
|
<PaginatedDataList
|
||||||
hasContentLoading={isLoading}
|
contentError={error}
|
||||||
items={tokens}
|
hasContentLoading={isLoading || isDeleteLoading}
|
||||||
itemCount={itemCount}
|
items={tokens}
|
||||||
pluralizedItemName={i18n._(t`Tokens`)}
|
itemCount={itemCount}
|
||||||
qsConfig={QS_CONFIG}
|
pluralizedItemName={i18n._(t`Tokens`)}
|
||||||
onRowClick={handleSelect}
|
qsConfig={QS_CONFIG}
|
||||||
toolbarSearchColumns={[
|
onRowClick={handleSelect}
|
||||||
{
|
toolbarSearchColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'application__name',
|
name: i18n._(t`Name`),
|
||||||
isDefault: true,
|
key: 'application__name',
|
||||||
},
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: i18n._(t`Description`),
|
{
|
||||||
key: 'description',
|
name: i18n._(t`Description`),
|
||||||
},
|
key: 'description',
|
||||||
]}
|
},
|
||||||
toolbarSortColumns={[
|
]}
|
||||||
{
|
toolbarSortColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'application__name',
|
name: i18n._(t`Name`),
|
||||||
},
|
key: 'application__name',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Scope`),
|
{
|
||||||
key: 'scope',
|
name: i18n._(t`Scope`),
|
||||||
},
|
key: 'scope',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Expires`),
|
{
|
||||||
key: 'expires',
|
name: i18n._(t`Expires`),
|
||||||
},
|
key: 'expires',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Created`),
|
{
|
||||||
key: 'created',
|
name: i18n._(t`Created`),
|
||||||
},
|
key: 'created',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Modified`),
|
{
|
||||||
key: 'modified',
|
name: i18n._(t`Modified`),
|
||||||
},
|
key: 'modified',
|
||||||
]}
|
},
|
||||||
renderToolbar={props => (
|
]}
|
||||||
<DataListToolbar
|
renderToolbar={props => (
|
||||||
{...props}
|
<DataListToolbar
|
||||||
showSelectAll
|
{...props}
|
||||||
isAllSelected={isAllSelected}
|
showSelectAll
|
||||||
qsConfig={QS_CONFIG}
|
isAllSelected={isAllSelected}
|
||||||
onSelectAll={isSelected => setSelected(isSelected ? [...tokens] : [])}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
onSelectAll={isSelected =>
|
||||||
...(canAdd
|
setSelected(isSelected ? [...tokens] : [])
|
||||||
? [
|
}
|
||||||
<ToolbarAddButton
|
additionalControls={[
|
||||||
key="add"
|
...(canAdd
|
||||||
linkTo={`${location.pathname}/add`}
|
? [
|
||||||
/>,
|
<ToolbarAddButton
|
||||||
]
|
key="add"
|
||||||
: []),
|
linkTo={`${location.pathname}/add`}
|
||||||
]}
|
/>,
|
||||||
/>
|
]
|
||||||
|
: []),
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
key="delete"
|
||||||
|
onDelete={handleDelete}
|
||||||
|
itemsToDelete={selected}
|
||||||
|
pluralizedItemName={i18n._(t`User tokens`)}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={token => (
|
||||||
|
<UserTokensListItem
|
||||||
|
key={token.id}
|
||||||
|
token={token}
|
||||||
|
onSelect={() => {
|
||||||
|
handleSelect(token);
|
||||||
|
}}
|
||||||
|
isSelected={selected.some(row => row.id === token.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
emptyStateControls={
|
||||||
|
canAdd ? (
|
||||||
|
<ToolbarAddButton key="add" linkTo={`${location.pathname}/add`} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{deletionError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={clearDeletionError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more user tokens.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
renderItem={token => (
|
</>
|
||||||
<UserTokensListItem
|
|
||||||
key={token.id}
|
|
||||||
token={token}
|
|
||||||
onSelect={() => {
|
|
||||||
handleSelect(token);
|
|
||||||
}}
|
|
||||||
isSelected={selected.some(row => row.id === token.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
emptyStateControls={
|
|
||||||
canAdd ? (
|
|
||||||
<ToolbarAddButton key="add" linkTo={`${location.pathname}/add`} />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { UsersAPI } from '../../../api';
|
import { UsersAPI, TokensAPI } from '../../../api';
|
||||||
import UserTokenList from './UserTokenList';
|
import UserTokenList from './UserTokenList';
|
||||||
|
|
||||||
jest.mock('../../../api/models/Users');
|
jest.mock('../../../api/models/Users');
|
||||||
|
jest.mock('../../../api/models/Tokens');
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
@@ -162,4 +163,96 @@ describe('<UserTokenList />', () => {
|
|||||||
wrapper.find('DataListCheck[id="select-token-3"]').props().checked
|
wrapper.find('DataListCheck[id="select-token-3"]').props().checked
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
test('delete button should be disabled', async () => {
|
||||||
|
UsersAPI.readTokens.mockResolvedValue(tokens);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<UserTokenList />);
|
||||||
|
});
|
||||||
|
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should select and then delete item properly', async () => {
|
||||||
|
UsersAPI.readTokens.mockResolvedValue(tokens);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<UserTokenList />);
|
||||||
|
});
|
||||||
|
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('DataListCheck[aria-labelledby="check-action-3"]')
|
||||||
|
.prop('onChange')(tokens.data.results[0]);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DataListCheck[aria-labelledby="check-action-3"]')
|
||||||
|
.prop('checked')
|
||||||
|
).toBe(true);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => expect(wrapper.find('AlertModal').length).toBe(1));
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
expect(TokensAPI.destroy).toHaveBeenCalledWith(3);
|
||||||
|
});
|
||||||
|
test('should select and then delete item properly', async () => {
|
||||||
|
UsersAPI.readTokens.mockResolvedValue(tokens);
|
||||||
|
TokensAPI.destroy.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/tokens',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<UserTokenList />);
|
||||||
|
});
|
||||||
|
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('DataListCheck[aria-labelledby="check-action-3"]')
|
||||||
|
.prop('onChange')(tokens.data.results[0]);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('DataListCheck[aria-labelledby="check-action-3"]')
|
||||||
|
.prop('checked')
|
||||||
|
).toBe(true);
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => expect(wrapper.find('AlertModal').length).toBe(1));
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
expect(TokensAPI.destroy).toHaveBeenCalledWith(3);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user