mirror of
https://github.com/ansible/awx.git
synced 2024-10-26 16:25:06 +03:00
Adds support for pending deletion on inventory list
This commit is contained in:
parent
d6201d9eb6
commit
4b566e9388
@ -2,18 +2,24 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
func,
|
||||
bool,
|
||||
node,
|
||||
number,
|
||||
string,
|
||||
arrayOf,
|
||||
shape,
|
||||
checkPropTypes,
|
||||
} from 'prop-types';
|
||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Alert, Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import AlertModal from '../AlertModal';
|
||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||
|
||||
const WarningMessage = styled(Alert)`
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const requireNameOrUsername = props => {
|
||||
const { name, username } = props;
|
||||
if (!name && !username) {
|
||||
@ -64,6 +70,7 @@ function ToolbarDeleteButton({
|
||||
pluralizedItemName,
|
||||
errorMessage,
|
||||
onDelete,
|
||||
warningMessage,
|
||||
i18n,
|
||||
}) {
|
||||
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||
@ -171,6 +178,9 @@ function ToolbarDeleteButton({
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
{warningMessage && (
|
||||
<WarningMessage variant="warning" isInline title={warningMessage} />
|
||||
)}
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
@ -182,11 +192,13 @@ ToolbarDeleteButton.propTypes = {
|
||||
itemsToDelete: arrayOf(ItemToDelete).isRequired,
|
||||
pluralizedItemName: string,
|
||||
errorMessage: string,
|
||||
warningMessage: node,
|
||||
};
|
||||
|
||||
ToolbarDeleteButton.defaultProps = {
|
||||
pluralizedItemName: 'Items',
|
||||
errorMessage: '',
|
||||
warningMessage: null,
|
||||
};
|
||||
|
||||
export default withI18n()(ToolbarDeleteButton);
|
||||
|
@ -7,6 +7,7 @@ exports[`<ToolbarDeleteButton /> should render button 1`] = `
|
||||
itemsToDelete={Array []}
|
||||
onDelete={[Function]}
|
||||
pluralizedItemName="Items"
|
||||
warningMessage={null}
|
||||
>
|
||||
<Tooltip
|
||||
content="Select a row to delete"
|
||||
|
@ -3,7 +3,6 @@ import { useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
@ -80,7 +79,13 @@ function InventoryList({ i18n }) {
|
||||
},
|
||||
[location.search] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
const inventories = useWsInventories(results, fetchInventoriesById);
|
||||
|
||||
const inventories = useWsInventories(
|
||||
results,
|
||||
fetchInventories,
|
||||
fetchInventoriesById,
|
||||
QS_CONFIG
|
||||
);
|
||||
|
||||
const isAllSelected =
|
||||
selected.length === inventories.length && selected.length > 0;
|
||||
@ -94,9 +99,7 @@ function InventoryList({ i18n }) {
|
||||
return Promise.all(selected.map(team => InventoriesAPI.destroy(team.id)));
|
||||
}, [selected]),
|
||||
{
|
||||
qsConfig: QS_CONFIG,
|
||||
allItemsSelected: isAllSelected,
|
||||
fetchItems: fetchInventories,
|
||||
}
|
||||
);
|
||||
|
||||
@ -113,10 +116,12 @@ function InventoryList({ i18n }) {
|
||||
};
|
||||
|
||||
const handleSelect = row => {
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
setSelected(selected.filter(s => s.id !== row.id));
|
||||
} else {
|
||||
setSelected(selected.concat(row));
|
||||
if (!row.pending_deletion) {
|
||||
if (selected.some(s => s.id === row.id)) {
|
||||
setSelected(selected.filter(s => s.id !== row.id));
|
||||
} else {
|
||||
setSelected(selected.concat(row));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -183,6 +188,10 @@ function InventoryList({ i18n }) {
|
||||
onDelete={handleInventoryDelete}
|
||||
itemsToDelete={selected}
|
||||
pluralizedItemName={i18n._(t`Inventories`)}
|
||||
warningMessage={i18n._(
|
||||
'{numItemsToDelete, plural, one {The inventory will be in a pending status until the final delete is processed.} other {The inventories will be in a pending status until the final delete is processed.}}',
|
||||
{ numItemsToDelete: selected.length }
|
||||
)}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Label,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
@ -69,6 +70,7 @@ function InventoryListItem({
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
id={`select-inventory-${inventory.id}`}
|
||||
isDisabled={inventory.pending_deletion}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={labelId}
|
||||
@ -79,52 +81,63 @@ function InventoryListItem({
|
||||
<SyncStatusIndicator status={syncStatus} />
|
||||
</DataListCell>,
|
||||
<DataListCell key="name">
|
||||
<Link to={`${detailUrl}`}>
|
||||
{inventory.pending_deletion ? (
|
||||
<b>{inventory.name}</b>
|
||||
</Link>
|
||||
) : (
|
||||
<Link to={`${detailUrl}`}>
|
||||
<b>{inventory.name}</b>
|
||||
</Link>
|
||||
)}
|
||||
</DataListCell>,
|
||||
<DataListCell key="kind">
|
||||
{inventory.kind === 'smart'
|
||||
? i18n._(t`Smart Inventory`)
|
||||
: i18n._(t`Inventory`)}
|
||||
</DataListCell>,
|
||||
inventory.pending_deletion && (
|
||||
<DataListCell alignRight isFilled={false} key="pending-delete">
|
||||
<Label color="red">{i18n._(t`Pending delete`)}</Label>
|
||||
</DataListCell>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
{inventory.summary_fields.user_capabilities.edit ? (
|
||||
<Tooltip content={i18n._(t`Edit Inventory`)} position="top">
|
||||
<Button
|
||||
{!inventory.pending_deletion && (
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
{inventory.summary_fields.user_capabilities.edit ? (
|
||||
<Tooltip content={i18n._(t`Edit Inventory`)} position="top">
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
aria-label={i18n._(t`Edit Inventory`)}
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`/inventories/${
|
||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory'
|
||||
}/${inventory.id}/edit`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{inventory.summary_fields.user_capabilities.copy && (
|
||||
<CopyButton
|
||||
copyItem={copyInventory}
|
||||
isDisabled={isDisabled}
|
||||
aria-label={i18n._(t`Edit Inventory`)}
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`/inventories/${
|
||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory'
|
||||
}/${inventory.id}/edit`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{inventory.summary_fields.user_capabilities.copy && (
|
||||
<CopyButton
|
||||
copyItem={copyInventory}
|
||||
isDisabled={isDisabled}
|
||||
onLoading={() => setIsDisabled(true)}
|
||||
onDoneLoading={() => setIsDisabled(false)}
|
||||
helperText={{
|
||||
tooltip: i18n._(t`Copy Inventory`),
|
||||
errorMessage: i18n._(t`Failed to copy inventory.`),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataListAction>
|
||||
onLoading={() => setIsDisabled(true)}
|
||||
onDoneLoading={() => setIsDisabled(false)}
|
||||
helperText={{
|
||||
tooltip: i18n._(t`Copy Inventory`),
|
||||
errorMessage: i18n._(t`Failed to copy inventory.`),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DataListAction>
|
||||
)}
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
|
@ -1,11 +1,21 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import {
|
||||
parseQueryString,
|
||||
replaceParams,
|
||||
encodeNonDefaultQueryString,
|
||||
} from '../../../util/qs';
|
||||
import useWebsocket from '../../../util/useWebsocket';
|
||||
import useThrottle from '../../../util/useThrottle';
|
||||
|
||||
export default function useWsInventories(
|
||||
initialInventories,
|
||||
fetchInventoriesById
|
||||
fetchInventories,
|
||||
fetchInventoriesById,
|
||||
qsConfig
|
||||
) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const [inventories, setInventories] = useState(initialInventories);
|
||||
const [inventoriesToFetch, setInventoriesToFetch] = useState([]);
|
||||
const throttledInventoriesToFetch = useThrottle(inventoriesToFetch, 5000);
|
||||
@ -53,7 +63,8 @@ export default function useWsInventories(
|
||||
function processWsMessage() {
|
||||
if (
|
||||
!lastMessage?.inventory_id ||
|
||||
lastMessage.type !== 'inventory_update'
|
||||
(lastMessage.type !== 'inventory_update' &&
|
||||
lastMessage.group_name !== 'inventories')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -64,16 +75,41 @@ export default function useWsInventories(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['pending', 'waiting', 'running'].includes(lastMessage.status)) {
|
||||
enqueueId(lastMessage.inventory_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const inventory = inventories[index];
|
||||
const updatedInventory = {
|
||||
...inventory,
|
||||
isSourceSyncRunning: true,
|
||||
};
|
||||
|
||||
if (lastMessage.group_name === 'inventories') {
|
||||
if (lastMessage.status === 'pending_deletion') {
|
||||
updatedInventory.pending_deletion = true;
|
||||
} else if (lastMessage.status === 'deleted') {
|
||||
if (inventories.length === 1) {
|
||||
const params = parseQueryString(qsConfig, location.search);
|
||||
if (params.page > 1) {
|
||||
const newParams = encodeNonDefaultQueryString(
|
||||
qsConfig,
|
||||
replaceParams(params, {
|
||||
page: params.page - 1,
|
||||
})
|
||||
);
|
||||
history.push(`${location.pathname}?${newParams}`);
|
||||
}
|
||||
} else {
|
||||
fetchInventories();
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!['pending', 'waiting', 'running'].includes(lastMessage.status)) {
|
||||
enqueueId(lastMessage.inventory_id);
|
||||
return;
|
||||
}
|
||||
|
||||
updatedInventory.isSourceSyncRunning = true;
|
||||
}
|
||||
|
||||
setInventories([
|
||||
...inventories.slice(0, index),
|
||||
updatedInventory,
|
||||
|
@ -16,9 +16,19 @@ jest.mock('../../../util/useThrottle', () => ({
|
||||
function TestInner() {
|
||||
return <div />;
|
||||
}
|
||||
function Test({ inventories, fetch }) {
|
||||
const syncedJobs = useWsInventories(inventories, fetch);
|
||||
return <TestInner inventories={syncedJobs} />;
|
||||
function Test({
|
||||
inventories,
|
||||
fetchInventories,
|
||||
fetchInventoriesById,
|
||||
qsConfig,
|
||||
}) {
|
||||
const syncedInventories = useWsInventories(
|
||||
inventories,
|
||||
fetchInventories,
|
||||
fetchInventoriesById,
|
||||
qsConfig
|
||||
);
|
||||
return <TestInner inventories={syncedInventories} />;
|
||||
}
|
||||
|
||||
describe('useWsInventories hook', () => {
|
||||
@ -105,10 +115,15 @@ describe('useWsInventories hook', () => {
|
||||
global.document.cookie = 'csrftoken=abc123';
|
||||
const mockServer = new WS('wss://localhost/websocket/');
|
||||
const inventories = [{ id: 1 }];
|
||||
const fetch = jest.fn(() => []);
|
||||
const fetchInventories = jest.fn(() => []);
|
||||
const fetchInventoriesById = jest.fn(() => []);
|
||||
await act(async () => {
|
||||
wrapper = await mountWithContexts(
|
||||
<Test inventories={inventories} fetch={fetch} />
|
||||
<Test
|
||||
inventories={inventories}
|
||||
fetchInventories={fetchInventories}
|
||||
fetchInventoriesById={fetchInventoriesById}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@ -123,7 +138,75 @@ describe('useWsInventories hook', () => {
|
||||
);
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith([1]);
|
||||
expect(fetchInventoriesById).toHaveBeenCalledWith([1]);
|
||||
WS.clean();
|
||||
});
|
||||
|
||||
test('should update inventory pending_deletion', async () => {
|
||||
global.document.cookie = 'csrftoken=abc123';
|
||||
const mockServer = new WS('wss://localhost/websocket/');
|
||||
|
||||
const inventories = [{ id: 1, pending_deletion: false }];
|
||||
await act(async () => {
|
||||
wrapper = await mountWithContexts(<Test inventories={inventories} />);
|
||||
});
|
||||
|
||||
await mockServer.connected;
|
||||
await expect(mockServer).toReceiveMessage(
|
||||
JSON.stringify({
|
||||
xrftoken: 'abc123',
|
||||
groups: {
|
||||
inventories: ['status_changed'],
|
||||
jobs: ['status_changed'],
|
||||
control: ['limit_reached_1'],
|
||||
},
|
||||
})
|
||||
);
|
||||
act(() => {
|
||||
mockServer.send(
|
||||
JSON.stringify({
|
||||
inventory_id: 1,
|
||||
group_name: 'inventories',
|
||||
status: 'pending_deletion',
|
||||
})
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('TestInner').prop('inventories')[0].pending_deletion
|
||||
).toEqual(true);
|
||||
WS.clean();
|
||||
});
|
||||
|
||||
test('should refetch inventories after an inventory is deleted', async () => {
|
||||
global.document.cookie = 'csrftoken=abc123';
|
||||
const mockServer = new WS('wss://localhost/websocket/');
|
||||
const inventories = [{ id: 1 }, { id: 2 }];
|
||||
const fetchInventories = jest.fn(() => []);
|
||||
const fetchInventoriesById = jest.fn(() => []);
|
||||
await act(async () => {
|
||||
wrapper = await mountWithContexts(
|
||||
<Test
|
||||
inventories={inventories}
|
||||
fetchInventories={fetchInventories}
|
||||
fetchInventoriesById={fetchInventoriesById}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await mockServer.connected;
|
||||
await act(async () => {
|
||||
mockServer.send(
|
||||
JSON.stringify({
|
||||
inventory_id: 1,
|
||||
group_name: 'inventories',
|
||||
status: 'deleted',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
expect(fetchInventories).toHaveBeenCalled();
|
||||
WS.clean();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user