mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 01:17:37 -02:30
Add Toast messages when resources are copied (#11758)
* create useToast hook * add copy success toast message to credentials/inventories * add Toast tests * add copy success toast to template/ee/project lists * move Toast type to types.js
This commit is contained in:
@@ -12,6 +12,7 @@ import useSelected from 'hooks/useSelected';
|
|||||||
import useExpanded from 'hooks/useExpanded';
|
import useExpanded from 'hooks/useExpanded';
|
||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import useWsTemplates from 'hooks/useWsTemplates';
|
import useWsTemplates from 'hooks/useWsTemplates';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import DatalistToolbar from '../DataListToolbar';
|
import DatalistToolbar from '../DataListToolbar';
|
||||||
@@ -41,6 +42,8 @@ function TemplateList({ defaultParams }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
results,
|
results,
|
||||||
@@ -123,6 +126,18 @@ function TemplateList({ defaultParams }) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(newTemplateId) => {
|
||||||
|
addToast({
|
||||||
|
id: newTemplateId,
|
||||||
|
title: t`Template copied successfully`,
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
hasTimeout: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
const handleTemplateDelete = async () => {
|
const handleTemplateDelete = async () => {
|
||||||
await deleteTemplates();
|
await deleteTemplates();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -266,6 +281,7 @@ function TemplateList({ defaultParams }) {
|
|||||||
onSelect={() => handleSelect(template)}
|
onSelect={() => handleSelect(template)}
|
||||||
isExpanded={expanded.some((row) => row.id === template.id)}
|
isExpanded={expanded.some((row) => row.id === template.id)}
|
||||||
onExpand={() => handleExpand(template)}
|
onExpand={() => handleExpand(template)}
|
||||||
|
onCopy={handleCopy}
|
||||||
isSelected={selected.some((row) => row.id === template.id)}
|
isSelected={selected.some((row) => row.id === template.id)}
|
||||||
fetchTemplates={fetchTemplates}
|
fetchTemplates={fetchTemplates}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
@@ -274,6 +290,7 @@ function TemplateList({ defaultParams }) {
|
|||||||
emptyStateControls={(canAddJT || canAddWFJT) && addButton}
|
emptyStateControls={(canAddJT || canAddWFJT) && addButton}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Toast {...toastProps} />
|
||||||
<AlertModal
|
<AlertModal
|
||||||
aria-label={t`Deletion Error`}
|
aria-label={t`Deletion Error`}
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function TemplateListItem({
|
|||||||
template,
|
template,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCopy,
|
||||||
detailUrl,
|
detailUrl,
|
||||||
fetchTemplates,
|
fetchTemplates,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
@@ -52,17 +53,21 @@ function TemplateListItem({
|
|||||||
)}/html/upgrade-migration-guide/upgrade_to_ees.html`;
|
)}/html/upgrade-migration-guide/upgrade_to_ees.html`;
|
||||||
|
|
||||||
const copyTemplate = useCallback(async () => {
|
const copyTemplate = useCallback(async () => {
|
||||||
|
let response;
|
||||||
if (template.type === 'job_template') {
|
if (template.type === 'job_template') {
|
||||||
await JobTemplatesAPI.copy(template.id, {
|
response = await JobTemplatesAPI.copy(template.id, {
|
||||||
name: `${template.name} @ ${timeOfDay()}`,
|
name: `${template.name} @ ${timeOfDay()}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await WorkflowJobTemplatesAPI.copy(template.id, {
|
response = await WorkflowJobTemplatesAPI.copy(template.id, {
|
||||||
name: `${template.name} @ ${timeOfDay()}`,
|
name: `${template.name} @ ${timeOfDay()}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (response.status === 201) {
|
||||||
|
onCopy(response.data.id);
|
||||||
|
}
|
||||||
await fetchTemplates();
|
await fetchTemplates();
|
||||||
}, [fetchTemplates, template.id, template.name, template.type]);
|
}, [fetchTemplates, template.id, template.name, template.type, onCopy]);
|
||||||
|
|
||||||
const handleCopyStart = useCallback(() => {
|
const handleCopyStart = useCallback(() => {
|
||||||
setIsDisabled(true);
|
setIsDisabled(true);
|
||||||
|
|||||||
64
awx/ui/src/hooks/useToast.js
Normal file
64
awx/ui/src/hooks/useToast.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
AlertGroup,
|
||||||
|
Alert,
|
||||||
|
AlertActionCloseButton,
|
||||||
|
AlertVariant,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { arrayOf, func } from 'prop-types';
|
||||||
|
import { Toast as ToastType } from 'types';
|
||||||
|
|
||||||
|
export default function useToast() {
|
||||||
|
const [toasts, setToasts] = useState([]);
|
||||||
|
|
||||||
|
const addToast = useCallback((newToast) => {
|
||||||
|
setToasts((oldToasts) => [...oldToasts, newToast]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeToast = useCallback((toastId) => {
|
||||||
|
setToasts((oldToasts) => oldToasts.filter((t) => t.id !== toastId));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addToast,
|
||||||
|
removeToast,
|
||||||
|
Toast,
|
||||||
|
toastProps: {
|
||||||
|
toasts,
|
||||||
|
removeToast,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toast({ toasts, removeToast }) {
|
||||||
|
if (!toasts.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertGroup data-cy="toast-container" isToast>
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<Alert
|
||||||
|
actionClose={
|
||||||
|
<AlertActionCloseButton onClose={() => removeToast(toast.id)} />
|
||||||
|
}
|
||||||
|
onTimeout={() => removeToast(toast.id)}
|
||||||
|
timeout={toast.hasTimeout}
|
||||||
|
title={toast.title}
|
||||||
|
variant={toast.variant}
|
||||||
|
key={`toast-message-${toast.id}`}
|
||||||
|
ouiaId={`toast-message-${toast.id}`}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</AlertGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.propTypes = {
|
||||||
|
toasts: arrayOf(ToastType).isRequired,
|
||||||
|
removeToast: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AlertVariant };
|
||||||
124
awx/ui/src/hooks/useToast.test.js
Normal file
124
awx/ui/src/hooks/useToast.test.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { shallow, mount } from 'enzyme';
|
||||||
|
import useToast, { Toast, AlertVariant } from './useToast';
|
||||||
|
|
||||||
|
describe('useToast', () => {
|
||||||
|
const Child = () => <div />;
|
||||||
|
const Test = () => {
|
||||||
|
const toastVals = useToast();
|
||||||
|
return <Child {...toastVals} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should provide Toast component', () => {
|
||||||
|
const wrapper = mount(<Test />);
|
||||||
|
|
||||||
|
expect(wrapper.find('Child').prop('Toast')).toEqual(Toast);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add toast', () => {
|
||||||
|
const wrapper = mount(<Test />);
|
||||||
|
|
||||||
|
expect(wrapper.find('Child').prop('toastProps').toasts).toEqual([]);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Child').prop('addToast')({
|
||||||
|
message: 'one',
|
||||||
|
id: 1,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Child').prop('toastProps').toasts).toEqual([
|
||||||
|
{
|
||||||
|
message: 'one',
|
||||||
|
id: 1,
|
||||||
|
variant: 'success',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove toast', () => {
|
||||||
|
const wrapper = mount(<Test />);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Child').prop('addToast')({
|
||||||
|
message: 'one',
|
||||||
|
id: 1,
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Child').prop('toastProps').toasts).toHaveLength(1);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('Child').prop('removeToast')(1);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Child').prop('toastProps').toasts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Toast', () => {
|
||||||
|
test('should render nothing with no toasts', () => {
|
||||||
|
const wrapper = shallow(<Toast toasts={[]} removeToast={() => {}} />);
|
||||||
|
expect(wrapper).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render toast alert', () => {
|
||||||
|
const toast = {
|
||||||
|
title: 'Inventory saved',
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
id: 1,
|
||||||
|
message: 'the message',
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<Toast toasts={[toast]} removeToast={() => {}} />);
|
||||||
|
|
||||||
|
const alert = wrapper.find('Alert');
|
||||||
|
expect(alert.prop('title')).toEqual('Inventory saved');
|
||||||
|
expect(alert.prop('variant')).toEqual('success');
|
||||||
|
expect(alert.prop('ouiaId')).toEqual('toast-message-1');
|
||||||
|
expect(alert.prop('children')).toEqual('the message');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call removeToast', () => {
|
||||||
|
const removeToast = jest.fn();
|
||||||
|
const toast = {
|
||||||
|
title: 'Inventory saved',
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
const wrapper = shallow(
|
||||||
|
<Toast toasts={[toast]} removeToast={removeToast} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const alert = wrapper.find('Alert');
|
||||||
|
alert.prop('actionClose').props.onClose(1);
|
||||||
|
expect(removeToast).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render multiple alerts', () => {
|
||||||
|
const toasts = [
|
||||||
|
{
|
||||||
|
title: 'Inventory saved',
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
id: 1,
|
||||||
|
message: 'the message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'error saving',
|
||||||
|
variant: AlertVariant.danger,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const wrapper = shallow(<Toast toasts={toasts} removeToast={() => {}} />);
|
||||||
|
|
||||||
|
const alert = wrapper.find('Alert');
|
||||||
|
expect(alert).toHaveLength(2);
|
||||||
|
|
||||||
|
expect(alert.at(0).prop('title')).toEqual('Inventory saved');
|
||||||
|
expect(alert.at(0).prop('variant')).toEqual('success');
|
||||||
|
expect(alert.at(1).prop('title')).toEqual('error saving');
|
||||||
|
expect(alert.at(1).prop('variant')).toEqual('danger');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import { t, Plural } from '@lingui/macro';
|
|||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { CredentialsAPI } from 'api';
|
import { CredentialsAPI } from 'api';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
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';
|
||||||
@@ -27,6 +28,8 @@ const QS_CONFIG = getQSConfig('credential', {
|
|||||||
|
|
||||||
function CredentialList() {
|
function CredentialList() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
credentials,
|
credentials,
|
||||||
@@ -104,100 +107,116 @@ function CredentialList() {
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(newCredentialId) => {
|
||||||
|
addToast({
|
||||||
|
id: newCredentialId,
|
||||||
|
title: t`Credential copied successfully`,
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
hasTimeout: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
|
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
|
||||||
selected[0]
|
selected[0]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<>
|
||||||
<Card>
|
<PageSection>
|
||||||
<PaginatedTable
|
<Card>
|
||||||
contentError={contentError}
|
<PaginatedTable
|
||||||
hasContentLoading={isLoading || isDeleteLoading}
|
contentError={contentError}
|
||||||
items={credentials}
|
hasContentLoading={isLoading || isDeleteLoading}
|
||||||
itemCount={credentialCount}
|
items={credentials}
|
||||||
qsConfig={QS_CONFIG}
|
itemCount={credentialCount}
|
||||||
clearSelected={clearSelected}
|
qsConfig={QS_CONFIG}
|
||||||
toolbarSearchableKeys={searchableKeys}
|
clearSelected={clearSelected}
|
||||||
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
toolbarSearchableKeys={searchableKeys}
|
||||||
toolbarSearchColumns={[
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
{
|
toolbarSearchColumns={[
|
||||||
name: t`Name`,
|
{
|
||||||
key: 'name__icontains',
|
name: t`Name`,
|
||||||
isDefault: true,
|
key: 'name__icontains',
|
||||||
},
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: t`Description`,
|
{
|
||||||
key: 'description__icontains',
|
name: t`Description`,
|
||||||
},
|
key: 'description__icontains',
|
||||||
{
|
},
|
||||||
name: t`Created By (Username)`,
|
{
|
||||||
key: 'created_by__username__icontains',
|
name: t`Created By (Username)`,
|
||||||
},
|
key: 'created_by__username__icontains',
|
||||||
{
|
},
|
||||||
name: t`Modified By (Username)`,
|
{
|
||||||
key: 'modified_by__username__icontains',
|
name: t`Modified By (Username)`,
|
||||||
},
|
key: 'modified_by__username__icontains',
|
||||||
]}
|
},
|
||||||
headerRow={
|
]}
|
||||||
<HeaderRow qsConfig={QS_CONFIG}>
|
headerRow={
|
||||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
<HeaderRow qsConfig={QS_CONFIG}>
|
||||||
<HeaderCell>{t`Type`}</HeaderCell>
|
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
<HeaderCell>{t`Type`}</HeaderCell>
|
||||||
</HeaderRow>
|
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||||
}
|
</HeaderRow>
|
||||||
renderRow={(item, index) => (
|
}
|
||||||
<CredentialListItem
|
renderRow={(item, index) => (
|
||||||
key={item.id}
|
<CredentialListItem
|
||||||
credential={item}
|
key={item.id}
|
||||||
fetchCredentials={fetchCredentials}
|
credential={item}
|
||||||
detailUrl={`/credentials/${item.id}/details`}
|
fetchCredentials={fetchCredentials}
|
||||||
isSelected={selected.some((row) => row.id === item.id)}
|
detailUrl={`/credentials/${item.id}/details`}
|
||||||
onSelect={() => handleSelect(item)}
|
isSelected={selected.some((row) => row.id === item.id)}
|
||||||
rowIndex={index}
|
onSelect={() => handleSelect(item)}
|
||||||
/>
|
onCopy={handleCopy}
|
||||||
)}
|
rowIndex={index}
|
||||||
renderToolbar={(props) => (
|
/>
|
||||||
<DataListToolbar
|
)}
|
||||||
{...props}
|
renderToolbar={(props) => (
|
||||||
isAllSelected={isAllSelected}
|
<DataListToolbar
|
||||||
onSelectAll={selectAll}
|
{...props}
|
||||||
qsConfig={QS_CONFIG}
|
isAllSelected={isAllSelected}
|
||||||
additionalControls={[
|
onSelectAll={selectAll}
|
||||||
...(canAdd
|
qsConfig={QS_CONFIG}
|
||||||
? [<ToolbarAddButton key="add" linkTo="/credentials/add" />]
|
additionalControls={[
|
||||||
: []),
|
...(canAdd
|
||||||
<ToolbarDeleteButton
|
? [<ToolbarAddButton key="add" linkTo="/credentials/add" />]
|
||||||
key="delete"
|
: []),
|
||||||
onDelete={handleDelete}
|
<ToolbarDeleteButton
|
||||||
itemsToDelete={selected}
|
key="delete"
|
||||||
pluralizedItemName={t`Credentials`}
|
onDelete={handleDelete}
|
||||||
deleteDetailsRequests={deleteDetailsRequests}
|
itemsToDelete={selected}
|
||||||
deleteMessage={
|
pluralizedItemName={t`Credentials`}
|
||||||
<Plural
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
value={selected.length}
|
deleteMessage={
|
||||||
one="This credential is currently being used by other resources. Are you sure you want to delete it?"
|
<Plural
|
||||||
other="Deleting these credentials could impact other resources that rely on them. Are you sure you want to delete anyway?"
|
value={selected.length}
|
||||||
/>
|
one="This credential is currently being used by other resources. Are you sure you want to delete it?"
|
||||||
}
|
other="Deleting these credentials could impact other resources that rely on them. Are you sure you want to delete anyway?"
|
||||||
/>,
|
/>
|
||||||
]}
|
}
|
||||||
/>
|
/>,
|
||||||
)}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
)}
|
||||||
<AlertModal
|
/>
|
||||||
aria-label={t`Deletion Error`}
|
</Card>
|
||||||
isOpen={deletionError}
|
<AlertModal
|
||||||
variant="error"
|
aria-label={t`Deletion Error`}
|
||||||
title={t`Error!`}
|
isOpen={deletionError}
|
||||||
onClose={clearDeletionError}
|
variant="error"
|
||||||
>
|
title={t`Error!`}
|
||||||
{t`Failed to delete one or more credentials.`}
|
onClose={clearDeletionError}
|
||||||
<ErrorDetail error={deletionError} />
|
>
|
||||||
</AlertModal>
|
{t`Failed to delete one or more credentials.`}
|
||||||
</PageSection>
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
</PageSection>
|
||||||
|
<Toast {...toastProps} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function CredentialListItem({
|
|||||||
detailUrl,
|
detailUrl,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCopy,
|
||||||
fetchCredentials,
|
fetchCredentials,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
}) {
|
}) {
|
||||||
@@ -28,11 +28,14 @@ function CredentialListItem({
|
|||||||
const canEdit = credential.summary_fields.user_capabilities.edit;
|
const canEdit = credential.summary_fields.user_capabilities.edit;
|
||||||
|
|
||||||
const copyCredential = useCallback(async () => {
|
const copyCredential = useCallback(async () => {
|
||||||
await CredentialsAPI.copy(credential.id, {
|
const response = await CredentialsAPI.copy(credential.id, {
|
||||||
name: `${credential.name} @ ${timeOfDay()}`,
|
name: `${credential.name} @ ${timeOfDay()}`,
|
||||||
});
|
});
|
||||||
|
if (response.status === 201) {
|
||||||
|
onCopy(response.data.id);
|
||||||
|
}
|
||||||
await fetchCredentials();
|
await fetchCredentials();
|
||||||
}, [credential.id, credential.name, fetchCredentials]);
|
}, [credential.id, credential.name, fetchCredentials, onCopy]);
|
||||||
|
|
||||||
const handleCopyStart = useCallback(() => {
|
const handleCopyStart = useCallback(() => {
|
||||||
setIsDisabled(true);
|
setIsDisabled(true);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ExecutionEnvironmentsAPI } from 'api';
|
|||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
import PaginatedTable, {
|
import PaginatedTable, {
|
||||||
HeaderRow,
|
HeaderRow,
|
||||||
HeaderCell,
|
HeaderCell,
|
||||||
@@ -29,6 +30,7 @@ const QS_CONFIG = getQSConfig('execution_environments', {
|
|||||||
function ExecutionEnvironmentList() {
|
function ExecutionEnvironmentList() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
error: contentError,
|
error: contentError,
|
||||||
@@ -94,6 +96,18 @@ function ExecutionEnvironmentList() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(newId) => {
|
||||||
|
addToast({
|
||||||
|
id: newId,
|
||||||
|
title: t`Execution environment copied successfully`,
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
hasTimeout: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
await deleteExecutionEnvironments();
|
await deleteExecutionEnvironments();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -194,6 +208,7 @@ function ExecutionEnvironmentList() {
|
|||||||
executionEnvironment={executionEnvironment}
|
executionEnvironment={executionEnvironment}
|
||||||
detailUrl={`${match.url}/${executionEnvironment.id}/details`}
|
detailUrl={`${match.url}/${executionEnvironment.id}/details`}
|
||||||
onSelect={() => handleSelect(executionEnvironment)}
|
onSelect={() => handleSelect(executionEnvironment)}
|
||||||
|
onCopy={handleCopy}
|
||||||
isSelected={selected.some(
|
isSelected={selected.some(
|
||||||
(row) => row.id === executionEnvironment.id
|
(row) => row.id === executionEnvironment.id
|
||||||
)}
|
)}
|
||||||
@@ -218,6 +233,7 @@ function ExecutionEnvironmentList() {
|
|||||||
{t`Failed to delete one or more execution environments`}
|
{t`Failed to delete one or more execution environments`}
|
||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
|
<Toast {...toastProps} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,28 @@ function ExecutionEnvironmentListItem({
|
|||||||
detailUrl,
|
detailUrl,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCopy,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
fetchExecutionEnvironments,
|
fetchExecutionEnvironments,
|
||||||
}) {
|
}) {
|
||||||
const [isDisabled, setIsDisabled] = useState(false);
|
const [isDisabled, setIsDisabled] = useState(false);
|
||||||
|
|
||||||
const copyExecutionEnvironment = useCallback(async () => {
|
const copyExecutionEnvironment = useCallback(async () => {
|
||||||
await ExecutionEnvironmentsAPI.copy(executionEnvironment.id, {
|
const response = await ExecutionEnvironmentsAPI.copy(
|
||||||
name: `${executionEnvironment.name} @ ${timeOfDay()}`,
|
executionEnvironment.id,
|
||||||
});
|
{
|
||||||
|
name: `${executionEnvironment.name} @ ${timeOfDay()}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.status === 201) {
|
||||||
|
onCopy(response.data.id);
|
||||||
|
}
|
||||||
await fetchExecutionEnvironments();
|
await fetchExecutionEnvironments();
|
||||||
}, [
|
}, [
|
||||||
executionEnvironment.id,
|
executionEnvironment.id,
|
||||||
executionEnvironment.name,
|
executionEnvironment.name,
|
||||||
fetchExecutionEnvironments,
|
fetchExecutionEnvironments,
|
||||||
|
onCopy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCopyStart = useCallback(() => {
|
const handleCopyStart = useCallback(() => {
|
||||||
@@ -114,6 +122,7 @@ ExecutionEnvironmentListItem.prototype = {
|
|||||||
detailUrl: string.isRequired,
|
detailUrl: string.isRequired,
|
||||||
isSelected: bool.isRequired,
|
isSelected: bool.isRequired,
|
||||||
onSelect: func.isRequired,
|
onSelect: func.isRequired,
|
||||||
|
onCopy: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExecutionEnvironmentListItem;
|
export default ExecutionEnvironmentListItem;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
|
|||||||
import { InventoriesAPI } from 'api';
|
import { InventoriesAPI } from 'api';
|
||||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import DatalistToolbar from 'components/DataListToolbar';
|
import DatalistToolbar from 'components/DataListToolbar';
|
||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
@@ -29,6 +30,7 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
function InventoryList() {
|
function InventoryList() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
@@ -112,6 +114,18 @@ function InventoryList() {
|
|||||||
clearSelected();
|
clearSelected();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(newInventoryId) => {
|
||||||
|
addToast({
|
||||||
|
id: newInventoryId,
|
||||||
|
title: t`Inventory copied successfully`,
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
hasTimeout: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
const hasContentLoading = isDeleteLoading || isLoading;
|
const hasContentLoading = isDeleteLoading || isLoading;
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
|
||||||
@@ -149,130 +163,134 @@ function InventoryList() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<>
|
||||||
<Card>
|
<PageSection>
|
||||||
<PaginatedTable
|
<Card>
|
||||||
contentError={contentError}
|
<PaginatedTable
|
||||||
hasContentLoading={hasContentLoading}
|
contentError={contentError}
|
||||||
items={inventories}
|
hasContentLoading={hasContentLoading}
|
||||||
itemCount={itemCount}
|
items={inventories}
|
||||||
pluralizedItemName={t`Inventories`}
|
itemCount={itemCount}
|
||||||
qsConfig={QS_CONFIG}
|
pluralizedItemName={t`Inventories`}
|
||||||
toolbarSearchColumns={[
|
qsConfig={QS_CONFIG}
|
||||||
{
|
toolbarSearchColumns={[
|
||||||
name: t`Name`,
|
{
|
||||||
key: 'name__icontains',
|
name: t`Name`,
|
||||||
isDefault: true,
|
key: 'name__icontains',
|
||||||
},
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: t`Inventory Type`,
|
{
|
||||||
key: 'or__kind',
|
name: t`Inventory Type`,
|
||||||
options: [
|
key: 'or__kind',
|
||||||
['', t`Inventory`],
|
options: [
|
||||||
['smart', t`Smart Inventory`],
|
['', t`Inventory`],
|
||||||
],
|
['smart', t`Smart Inventory`],
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: t`Organization`,
|
{
|
||||||
key: 'organization__name',
|
name: t`Organization`,
|
||||||
},
|
key: 'organization__name',
|
||||||
{
|
},
|
||||||
name: t`Description`,
|
{
|
||||||
key: 'description__icontains',
|
name: t`Description`,
|
||||||
},
|
key: 'description__icontains',
|
||||||
{
|
},
|
||||||
name: t`Created By (Username)`,
|
{
|
||||||
key: 'created_by__username__icontains',
|
name: t`Created By (Username)`,
|
||||||
},
|
key: 'created_by__username__icontains',
|
||||||
{
|
},
|
||||||
name: t`Modified By (Username)`,
|
{
|
||||||
key: 'modified_by__username__icontains',
|
name: t`Modified By (Username)`,
|
||||||
},
|
key: 'modified_by__username__icontains',
|
||||||
]}
|
},
|
||||||
toolbarSortColumns={[
|
]}
|
||||||
{
|
toolbarSortColumns={[
|
||||||
name: t`Name`,
|
{
|
||||||
key: 'name',
|
name: t`Name`,
|
||||||
},
|
key: 'name',
|
||||||
]}
|
},
|
||||||
toolbarSearchableKeys={searchableKeys}
|
]}
|
||||||
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
toolbarSearchableKeys={searchableKeys}
|
||||||
clearSelected={clearSelected}
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
headerRow={
|
clearSelected={clearSelected}
|
||||||
<HeaderRow qsConfig={QS_CONFIG}>
|
headerRow={
|
||||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
<HeaderRow qsConfig={QS_CONFIG}>
|
||||||
<HeaderCell>{t`Status`}</HeaderCell>
|
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||||
<HeaderCell>{t`Type`}</HeaderCell>
|
<HeaderCell>{t`Status`}</HeaderCell>
|
||||||
<HeaderCell>{t`Organization`}</HeaderCell>
|
<HeaderCell>{t`Type`}</HeaderCell>
|
||||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
<HeaderCell>{t`Organization`}</HeaderCell>
|
||||||
</HeaderRow>
|
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||||
}
|
</HeaderRow>
|
||||||
renderToolbar={(props) => (
|
}
|
||||||
<DatalistToolbar
|
renderToolbar={(props) => (
|
||||||
{...props}
|
<DatalistToolbar
|
||||||
isAllSelected={isAllSelected}
|
{...props}
|
||||||
onSelectAll={selectAll}
|
isAllSelected={isAllSelected}
|
||||||
qsConfig={QS_CONFIG}
|
onSelectAll={selectAll}
|
||||||
additionalControls={[
|
qsConfig={QS_CONFIG}
|
||||||
...(canAdd ? [addButton] : []),
|
additionalControls={[
|
||||||
<ToolbarDeleteButton
|
...(canAdd ? [addButton] : []),
|
||||||
key="delete"
|
<ToolbarDeleteButton
|
||||||
onDelete={handleInventoryDelete}
|
key="delete"
|
||||||
itemsToDelete={selected}
|
onDelete={handleInventoryDelete}
|
||||||
pluralizedItemName={t`Inventories`}
|
itemsToDelete={selected}
|
||||||
deleteDetailsRequests={deleteDetailsRequests}
|
pluralizedItemName={t`Inventories`}
|
||||||
deleteMessage={
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
<Plural
|
deleteMessage={
|
||||||
value={selected.length}
|
<Plural
|
||||||
one="This inventory is currently being used by some templates. Are you sure you want to delete it?"
|
value={selected.length}
|
||||||
other="Deleting these inventories could impact some templates that rely on them. Are you sure you want to delete anyway?"
|
one="This inventory is currently being used by some templates. Are you sure you want to delete it?"
|
||||||
/>
|
other="Deleting these inventories could impact some templates that rely on them. Are you sure you want to delete anyway?"
|
||||||
}
|
/>
|
||||||
warningMessage={
|
}
|
||||||
<Plural
|
warningMessage={
|
||||||
value={selected.length}
|
<Plural
|
||||||
one="The inventory will be in a pending status until the final delete is processed."
|
value={selected.length}
|
||||||
other="The inventories will be in a pending status until the final delete is processed."
|
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."
|
||||||
}
|
/>
|
||||||
/>,
|
}
|
||||||
]}
|
/>,
|
||||||
/>
|
]}
|
||||||
)}
|
/>
|
||||||
renderRow={(inventory, index) => (
|
)}
|
||||||
<InventoryListItem
|
renderRow={(inventory, index) => (
|
||||||
key={inventory.id}
|
<InventoryListItem
|
||||||
value={inventory.name}
|
key={inventory.id}
|
||||||
inventory={inventory}
|
value={inventory.name}
|
||||||
rowIndex={index}
|
inventory={inventory}
|
||||||
fetchInventories={fetchInventories}
|
rowIndex={index}
|
||||||
detailUrl={
|
fetchInventories={fetchInventories}
|
||||||
inventory.kind === 'smart'
|
detailUrl={
|
||||||
? `${match.url}/smart_inventory/${inventory.id}/details`
|
inventory.kind === 'smart'
|
||||||
: `${match.url}/inventory/${inventory.id}/details`
|
? `${match.url}/smart_inventory/${inventory.id}/details`
|
||||||
}
|
: `${match.url}/inventory/${inventory.id}/details`
|
||||||
onSelect={() => {
|
|
||||||
if (!inventory.pending_deletion) {
|
|
||||||
handleSelect(inventory);
|
|
||||||
}
|
}
|
||||||
}}
|
onSelect={() => {
|
||||||
isSelected={selected.some((row) => row.id === inventory.id)}
|
if (!inventory.pending_deletion) {
|
||||||
/>
|
handleSelect(inventory);
|
||||||
)}
|
}
|
||||||
emptyStateControls={canAdd && addButton}
|
}}
|
||||||
/>
|
onCopy={handleCopy}
|
||||||
</Card>
|
isSelected={selected.some((row) => row.id === inventory.id)}
|
||||||
<AlertModal
|
/>
|
||||||
isOpen={deletionError}
|
)}
|
||||||
variant="error"
|
emptyStateControls={canAdd && addButton}
|
||||||
aria-label={t`Deletion Error`}
|
/>
|
||||||
title={t`Error!`}
|
</Card>
|
||||||
onClose={clearDeletionError}
|
<AlertModal
|
||||||
>
|
isOpen={deletionError}
|
||||||
{t`Failed to delete one or more inventories.`}
|
variant="error"
|
||||||
<ErrorDetail error={deletionError} />
|
aria-label={t`Deletion Error`}
|
||||||
</AlertModal>
|
title={t`Error!`}
|
||||||
</PageSection>
|
onClose={clearDeletionError}
|
||||||
|
>
|
||||||
|
{t`Failed to delete one or more inventories.`}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
</PageSection>
|
||||||
|
<Toast {...toastProps} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function InventoryListItem({
|
|||||||
rowIndex,
|
rowIndex,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCopy,
|
||||||
detailUrl,
|
detailUrl,
|
||||||
fetchInventories,
|
fetchInventories,
|
||||||
}) {
|
}) {
|
||||||
@@ -30,11 +31,14 @@ function InventoryListItem({
|
|||||||
const [isCopying, setIsCopying] = useState(false);
|
const [isCopying, setIsCopying] = useState(false);
|
||||||
|
|
||||||
const copyInventory = useCallback(async () => {
|
const copyInventory = useCallback(async () => {
|
||||||
await InventoriesAPI.copy(inventory.id, {
|
const response = await InventoriesAPI.copy(inventory.id, {
|
||||||
name: `${inventory.name} @ ${timeOfDay()}`,
|
name: `${inventory.name} @ ${timeOfDay()}`,
|
||||||
});
|
});
|
||||||
|
if (response.status === 201) {
|
||||||
|
onCopy(response.data.id);
|
||||||
|
}
|
||||||
await fetchInventories();
|
await fetchInventories();
|
||||||
}, [inventory.id, inventory.name, fetchInventories]);
|
}, [inventory.id, inventory.name, fetchInventories, onCopy]);
|
||||||
|
|
||||||
const handleCopyStart = useCallback(() => {
|
const handleCopyStart = useCallback(() => {
|
||||||
setIsCopying(true);
|
setIsCopying(true);
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useLocation, useRouteMatch } from 'react-router-dom';
|
import { useLocation, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
Alert,
|
|
||||||
AlertActionCloseButton,
|
|
||||||
AlertGroup,
|
|
||||||
Card,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { NotificationTemplatesAPI } from 'api';
|
import { NotificationTemplatesAPI } from 'api';
|
||||||
import PaginatedTable, {
|
import PaginatedTable, {
|
||||||
HeaderRow,
|
HeaderRow,
|
||||||
@@ -22,6 +16,7 @@ import ErrorDetail from 'components/ErrorDetail';
|
|||||||
import DataListToolbar from 'components/DataListToolbar';
|
import DataListToolbar from 'components/DataListToolbar';
|
||||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import NotificationTemplateListItem from './NotificationTemplateListItem';
|
import NotificationTemplateListItem from './NotificationTemplateListItem';
|
||||||
|
|
||||||
@@ -34,7 +29,8 @@ const QS_CONFIG = getQSConfig('notification-templates', {
|
|||||||
function NotificationTemplatesList() {
|
function NotificationTemplatesList() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
const [testToasts, setTestToasts] = useState([]);
|
// const [testToasts, setTestToasts] = useState([]);
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const addUrl = `${match.url}/add`;
|
const addUrl = `${match.url}/add`;
|
||||||
|
|
||||||
@@ -107,18 +103,7 @@ function NotificationTemplatesList() {
|
|||||||
clearSelected();
|
clearSelected();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTestToast = useCallback((notification) => {
|
|
||||||
setTestToasts((oldToasts) => [...oldToasts, notification]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeTestToast = (notificationId) => {
|
|
||||||
setTestToasts((oldToasts) =>
|
|
||||||
oldToasts.filter((toast) => toast.id !== notificationId)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
const alertGroupDataCy = 'notification-template-alerts';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -198,7 +183,35 @@ function NotificationTemplatesList() {
|
|||||||
}
|
}
|
||||||
renderRow={(template, index) => (
|
renderRow={(template, index) => (
|
||||||
<NotificationTemplateListItem
|
<NotificationTemplateListItem
|
||||||
onAddToast={addTestToast}
|
onAddToast={(notification) => {
|
||||||
|
if (notification.status === 'pending') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message;
|
||||||
|
if (notification.status === 'successful') {
|
||||||
|
message = t`Notification sent successfully`;
|
||||||
|
}
|
||||||
|
if (notification.status === 'failed') {
|
||||||
|
if (notification?.error === 'timed out') {
|
||||||
|
message = t`Notification timed out`;
|
||||||
|
} else {
|
||||||
|
message = notification.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToast({
|
||||||
|
id: notification.id,
|
||||||
|
title:
|
||||||
|
notification.summary_fields.notification_template.name,
|
||||||
|
variant:
|
||||||
|
notification.status === 'failed'
|
||||||
|
? AlertVariant.danger
|
||||||
|
: AlertVariant.success,
|
||||||
|
hasTimeout: notification.status !== 'failed',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}}
|
||||||
key={template.id}
|
key={template.id}
|
||||||
fetchTemplates={fetchTemplates}
|
fetchTemplates={fetchTemplates}
|
||||||
template={template}
|
template={template}
|
||||||
@@ -223,39 +236,7 @@ function NotificationTemplatesList() {
|
|||||||
{t`Failed to delete one or more notification template.`}
|
{t`Failed to delete one or more notification template.`}
|
||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
<AlertGroup data-cy={alertGroupDataCy} isToast>
|
<Toast {...toastProps} />
|
||||||
{testToasts
|
|
||||||
.filter((notification) => notification.status !== 'pending')
|
|
||||||
.map((notification) => (
|
|
||||||
<Alert
|
|
||||||
actionClose={
|
|
||||||
<AlertActionCloseButton
|
|
||||||
onClose={() => removeTestToast(notification.id)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onTimeout={() => removeTestToast(notification.id)}
|
|
||||||
timeout={notification.status !== 'failed'}
|
|
||||||
title={notification.summary_fields.notification_template.name}
|
|
||||||
variant={notification.status === 'failed' ? 'danger' : 'success'}
|
|
||||||
key={`notification-template-alert-${notification.id}`}
|
|
||||||
ouiaId={`notification-template-alert-${notification.id}`}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{notification.status === 'successful' && (
|
|
||||||
<p>{t`Notification sent successfully`}</p>
|
|
||||||
)}
|
|
||||||
{notification.status === 'failed' &&
|
|
||||||
notification?.error === 'timed out' && (
|
|
||||||
<p>{t`Notification timed out`}</p>
|
|
||||||
)}
|
|
||||||
{notification.status === 'failed' &&
|
|
||||||
notification?.error !== 'timed out' && (
|
|
||||||
<p>{notification.error}</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</Alert>
|
|
||||||
))}
|
|
||||||
</AlertGroup>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import PaginatedTable, {
|
|||||||
} from 'components/PaginatedTable';
|
} from 'components/PaginatedTable';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
import useExpanded from 'hooks/useExpanded';
|
import useExpanded from 'hooks/useExpanded';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||||
import useWsProjects from './useWsProjects';
|
import useWsProjects from './useWsProjects';
|
||||||
@@ -34,6 +35,7 @@ const QS_CONFIG = getQSConfig('project', {
|
|||||||
function ProjectList() {
|
function ProjectList() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
request: fetchUpdatedProject,
|
request: fetchUpdatedProject,
|
||||||
@@ -123,6 +125,18 @@ function ProjectList() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(newId) => {
|
||||||
|
addToast({
|
||||||
|
id: newId,
|
||||||
|
title: t`Project copied successfully`,
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
hasTimeout: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
const handleProjectDelete = async () => {
|
const handleProjectDelete = async () => {
|
||||||
await deleteProjects();
|
await deleteProjects();
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
@@ -255,6 +269,7 @@ function ProjectList() {
|
|||||||
detailUrl={`${match.url}/${project.id}`}
|
detailUrl={`${match.url}/${project.id}`}
|
||||||
isSelected={selected.some((row) => row.id === project.id)}
|
isSelected={selected.some((row) => row.id === project.id)}
|
||||||
onSelect={() => handleSelect(project)}
|
onSelect={() => handleSelect(project)}
|
||||||
|
onCopy={handleCopy}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
onRefreshRow={(projectId) => fetchUpdatedProject(projectId)}
|
onRefreshRow={(projectId) => fetchUpdatedProject(projectId)}
|
||||||
/>
|
/>
|
||||||
@@ -267,6 +282,7 @@ function ProjectList() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
<Toast {...toastProps} />
|
||||||
{deletionError && (
|
{deletionError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function ProjectListItem({
|
|||||||
project,
|
project,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCopy,
|
||||||
detailUrl,
|
detailUrl,
|
||||||
fetchProjects,
|
fetchProjects,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
@@ -53,11 +54,14 @@ function ProjectListItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copyProject = useCallback(async () => {
|
const copyProject = useCallback(async () => {
|
||||||
await ProjectsAPI.copy(project.id, {
|
const response = await ProjectsAPI.copy(project.id, {
|
||||||
name: `${project.name} @ ${timeOfDay()}`,
|
name: `${project.name} @ ${timeOfDay()}`,
|
||||||
});
|
});
|
||||||
|
if (response.status === 201) {
|
||||||
|
onCopy(response.data.id);
|
||||||
|
}
|
||||||
await fetchProjects();
|
await fetchProjects();
|
||||||
}, [project.id, project.name, fetchProjects]);
|
}, [project.id, project.name, fetchProjects, onCopy]);
|
||||||
|
|
||||||
const generateLastJobTooltip = (job) => (
|
const generateLastJobTooltip = (job) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
oneOf,
|
oneOf,
|
||||||
oneOfType,
|
oneOfType,
|
||||||
} from 'prop-types';
|
} from 'prop-types';
|
||||||
|
import { AlertVariant } from '@patternfly/react-core';
|
||||||
|
|
||||||
export const Role = shape({
|
export const Role = shape({
|
||||||
descendent_roles: arrayOf(string),
|
descendent_roles: arrayOf(string),
|
||||||
@@ -428,3 +429,11 @@ export const SearchableKeys = arrayOf(
|
|||||||
type: string.isRequired,
|
type: string.isRequired,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const Toast = shape({
|
||||||
|
title: string.isRequired,
|
||||||
|
variant: oneOf(Object.values(AlertVariant)).isRequired,
|
||||||
|
id: oneOfType([string, number]).isRequired,
|
||||||
|
hasTimeout: bool,
|
||||||
|
message: string,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user