mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
66c7ae6eb4
commit
0603ae6ca9
5 changed files with 77 additions and 20 deletions
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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.`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue