Don't allow duplicate saved views with the same name (#52040)

* Don't allow duplicate saved views with the same name

* Change logic to make it a little easier to reason about

* Change error names
This commit is contained in:
Phillip Burch 2019-12-04 17:44:51 -06:00 committed by GitHub
parent 66c7ae6eb4
commit 0603ae6ca9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 20 deletions

View file

@ -23,11 +23,12 @@ import {
} from '@elastic/eui';
interface Props {
isInvalid: boolean;
close(): void;
save(name: string, shouldIncludeTime: boolean): void;
}
export const SavedViewCreateModal = ({ close, save }: Props) => {
export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => {
const [viewName, setViewName] = useState('');
const [includeTime, setIncludeTime] = useState(false);
const onCheckChange = useCallback(e => setIncludeTime(e.target.checked), []);
@ -35,7 +36,6 @@ export const SavedViewCreateModal = ({ close, save }: Props) => {
const saveView = useCallback(() => {
save(viewName, includeTime);
close();
}, [viewName, includeTime]);
return (
@ -52,6 +52,7 @@ export const SavedViewCreateModal = ({ close, save }: Props) => {
<EuiModalBody>
<EuiFieldText
isInvalid={isInvalid}
placeholder={i18n.translate('xpack.infra.waffle.savedViews.viewNamePlaceholder', {
defaultMessage: 'Name',
})}

View file

@ -29,12 +29,17 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
find,
errorOnFind,
errorOnCreate,
createdId,
} = useSavedView(props.defaultViewState, props.viewType);
const [modalOpen, setModalOpen] = useState(false);
const [isInvalid, setIsInvalid] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const openSaveModal = useCallback(() => setCreateModalOpen(true), []);
const closeCreateModal = useCallback(() => setCreateModalOpen(false), []);
const openSaveModal = useCallback(() => {
setIsInvalid(false);
setCreateModalOpen(true);
}, []);
const closeModal = useCallback(() => setModalOpen(false), []);
const closeCreateModal = useCallback(() => setCreateModalOpen(false), []);
const loadViews = useCallback(() => {
find();
setModalOpen(true);
@ -50,6 +55,19 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
[props.viewState, saveView]
);
useEffect(() => {
if (errorOnCreate) {
setIsInvalid(true);
}
}, [errorOnCreate]);
useEffect(() => {
if (createdId !== undefined) {
// INFO: Close the modal after the view is created.
closeCreateModal();
}
}, [createdId, closeCreateModal]);
useEffect(() => {
if (deletedId !== undefined) {
// INFO: Refresh view list after an item is deleted
@ -59,9 +77,9 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
useEffect(() => {
if (errorOnCreate) {
toastNotifications.addWarning(getErrorToast('create')!);
toastNotifications.addWarning(getErrorToast('create', errorOnCreate)!);
} else if (errorOnFind) {
toastNotifications.addWarning(getErrorToast('find')!);
toastNotifications.addWarning(getErrorToast('find', errorOnFind)!);
}
}, [errorOnCreate, errorOnFind]);
@ -82,7 +100,9 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
</EuiButtonEmpty>
</EuiFlexGroup>
{createModalOpen && <SavedViewCreateModal close={closeCreateModal} save={save} />}
{createModalOpen && (
<SavedViewCreateModal isInvalid={isInvalid} close={closeCreateModal} save={save} />
)}
{modalOpen && (
<SavedViewListFlyout<ViewState>
loading={loading}
@ -96,18 +116,22 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
);
}
const getErrorToast = (type: 'create' | 'find') => {
const getErrorToast = (type: 'create' | 'find', msg?: string) => {
if (type === 'create') {
return {
title: i18n.translate('xpack.infra.savedView.errorOnCreate.title', {
defaultMessage: `An error occured saving view.`,
}),
title:
msg ||
i18n.translate('xpack.infra.savedView.errorOnCreate.title', {
defaultMessage: `An error occured saving view.`,
}),
};
} else if (type === 'find') {
return {
title: i18n.translate('xpack.infra.savedView.findError.title', {
defaultMessage: `An error occurred while loading views.`,
}),
title:
msg ||
i18n.translate('xpack.infra.savedView.findError.title', {
defaultMessage: `An error occurred while loading views.`,
}),
};
}
};

View file

@ -12,6 +12,7 @@ import { SavedObjectAttributes } from 'src/core/server';
export const useCreateSavedObject = (type: string) => {
const [data, setData] = useState<SimpleSavedObject<SavedObjectAttributes> | null>(null);
const [createdId, setCreatedId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
@ -21,6 +22,7 @@ export const useCreateSavedObject = (type: string) => {
const save = async () => {
try {
const d = await npStart.core.savedObjects.client.create(type, attributes, options);
setCreatedId(d.id);
setError(null);
setData(d);
setLoading(false);
@ -39,5 +41,6 @@ export const useCreateSavedObject = (type: string) => {
loading,
error,
create,
createdId,
};
};

View file

@ -37,7 +37,16 @@ export const useFindSavedObject = <SavedObjectType extends SavedObjectAttributes
[type]
);
const hasView = async (name: string) => {
const objects = await npStart.core.savedObjects.client.find<SavedObjectType>({
type,
});
return objects.savedObjects.filter(o => o.attributes.name === name).length > 0;
};
return {
hasView,
data,
loading,
error,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { useFindSavedObject } from './use_find_saved_object';
import { useCreateSavedObject } from './use_create_saved_object';
@ -16,18 +16,37 @@ export type SavedView<ViewState> = ViewState & {
isDefault?: boolean;
};
export type SavedViewSavedObject<ViewState> = ViewState & {
export type SavedViewSavedObject<ViewState = {}> = ViewState & {
name: string;
};
export const useSavedView = <ViewState>(defaultViewState: ViewState, viewType: string) => {
const { data, loading, find, error: errorOnFind } = useFindSavedObject<
const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject<
SavedViewSavedObject<ViewState>
>(viewType);
const { create, error: errorOnCreate } = useCreateSavedObject(viewType);
const { create, error: errorOnCreate, createdId } = useCreateSavedObject(viewType);
const { deleteObject, deletedId } = useDeleteSavedObject(viewType);
const deleteView = useCallback((id: string) => deleteObject(id), []);
const saveView = useCallback((d: { [p: string]: any }) => create(d), []);
const [createError, setCreateError] = useState<string | null>(null);
useEffect(() => setCreateError(createError), [errorOnCreate, setCreateError]);
const saveView = useCallback((d: { [p: string]: any }) => {
const doSave = async () => {
const exists = await hasView(d.name);
if (exists) {
setCreateError(
i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', {
defaultMessage: `A view with that name already exists.`,
})
);
return;
}
create(d);
};
setCreateError(null);
doSave();
}, []);
const savedObjects = data ? data.savedObjects : [];
const views = useMemo(() => {
@ -61,8 +80,9 @@ export const useSavedView = <ViewState>(defaultViewState: ViewState, viewType: s
saveView,
loading,
deletedId,
createdId,
errorOnFind,
errorOnCreate,
errorOnCreate: createError,
deleteView,
find,
};