mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
Folder: Replace folderId with folderUid (#58393)
* support folderuid in FolderPicker * support folderuid in unified alerting * support folderuid when returning to view mode after editing a panel * support folderuid when preselecting the folderpicker in dashboard general settings * support folderuid when saving dashboard * support folderuid when pre-selecting folderpicker in dashboard form * support folderuid in routes when loading a dashboard * support folderuid when saving dashboard json * support folderuid when validating new dashboard name * support folderuid when moving dashboard to another folder * support folderuid on dashboard action buttons * support folderuid when creating a new dashboard on an empty folder * support folderuid when showing library panel modal * support folderuid when saving library panel * support folderuid when importing dashboard * fixed broken tests * use folderuid when importing dashboards * remove commented line * fix typo when comparing uid values
This commit is contained in:
parent
ab36252c86
commit
27b6b3b3bd
@ -3227,12 +3227,11 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@ -3266,8 +3265,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@ -4622,8 +4620,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/search/page/components/MoveToFolderModal.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/search/page/components/SearchResultsCards.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
|
@ -16,8 +16,8 @@ describe('FolderPicker', () => {
|
||||
jest
|
||||
.spyOn(api, 'searchFolders')
|
||||
.mockResolvedValue([
|
||||
{ title: 'Dash 1', id: 1 } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', id: 2 } as DashboardSearchHit,
|
||||
{ title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||
]);
|
||||
|
||||
render(<FolderPicker onChange={jest.fn()} />);
|
||||
@ -28,12 +28,12 @@ describe('FolderPicker', () => {
|
||||
jest
|
||||
.spyOn(api, 'searchFolders')
|
||||
.mockResolvedValue([
|
||||
{ title: 'Dash 1', id: 1 } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', id: 2 } as DashboardSearchHit,
|
||||
{ title: 'Dash 3', id: 3 } as DashboardSearchHit,
|
||||
{ title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 3', uid: '7MeksYbmk' } as DashboardSearchHit,
|
||||
]);
|
||||
|
||||
render(<FolderPicker onChange={jest.fn()} filter={(hits) => hits.filter((h) => h.id !== 2)} />);
|
||||
render(<FolderPicker onChange={jest.fn()} filter={(hits) => hits.filter((h) => h.uid !== 'wfTJJL5Wz')} />);
|
||||
|
||||
const pickerContainer = screen.getByLabelText(selectors.components.FolderPicker.input);
|
||||
selectEvent.openMenu(pickerContainer);
|
||||
@ -46,13 +46,13 @@ describe('FolderPicker', () => {
|
||||
});
|
||||
|
||||
it('should allow creating a new option', async () => {
|
||||
const newFolder = { title: 'New Folder', id: 3 } as DashboardSearchHit;
|
||||
const newFolder = { title: 'New Folder', uid: '7MeksYbmk' } as DashboardSearchHit;
|
||||
|
||||
jest
|
||||
.spyOn(api, 'searchFolders')
|
||||
.mockResolvedValue([
|
||||
{ title: 'Dash 1', id: 1 } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', id: 2 } as DashboardSearchHit,
|
||||
{ title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||
]);
|
||||
|
||||
const onChangeFn = jest.fn();
|
||||
@ -70,7 +70,7 @@ describe('FolderPicker', () => {
|
||||
expect(create).toHaveBeenCalledWith({ title: newFolder.title });
|
||||
});
|
||||
|
||||
expect(onChangeFn).toHaveBeenCalledWith({ title: newFolder.title, id: newFolder.id });
|
||||
expect(onChangeFn).toHaveBeenCalledWith({ title: newFolder.title, uid: newFolder.uid });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(newFolder.title)).toBeInTheDocument();
|
||||
});
|
||||
@ -80,8 +80,8 @@ describe('FolderPicker', () => {
|
||||
jest
|
||||
.spyOn(api, 'searchFolders')
|
||||
.mockResolvedValue([
|
||||
{ title: 'Dash 1', id: 1 } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', id: 2 } as DashboardSearchHit,
|
||||
{ title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||
]);
|
||||
|
||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
|
||||
@ -101,8 +101,8 @@ describe('FolderPicker', () => {
|
||||
jest
|
||||
.spyOn(api, 'searchFolders')
|
||||
.mockResolvedValue([
|
||||
{ title: 'Dash 1', id: 1 } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', id: 2 } as DashboardSearchHit,
|
||||
{ title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||
]);
|
||||
|
||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
|
||||
@ -122,8 +122,8 @@ describe('FolderPicker', () => {
|
||||
jest
|
||||
.spyOn(api, 'searchFolders')
|
||||
.mockResolvedValue([
|
||||
{ title: 'Dash 1', id: 1 } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', id: 2 } as DashboardSearchHit,
|
||||
{ title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
|
||||
{ title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
|
||||
]);
|
||||
|
||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
||||
@ -141,28 +141,28 @@ describe('FolderPicker', () => {
|
||||
});
|
||||
|
||||
describe('getInitialValues', () => {
|
||||
describe('when called with folderId and title', () => {
|
||||
it('then it should return folderId and title', async () => {
|
||||
describe('when called with folderUid and title', () => {
|
||||
it('then it should return folderUid and title', async () => {
|
||||
const getFolder = jest.fn().mockResolvedValue({});
|
||||
const folder = await getInitialValues({ folderId: 0, folderName: 'Some title', getFolder });
|
||||
const folder = await getInitialValues({ folderUid: '', folderName: 'Some title', getFolder });
|
||||
|
||||
expect(folder).toEqual({ label: 'Some title', value: 0 });
|
||||
expect(folder).toEqual({ label: 'Some title', value: '' });
|
||||
expect(getFolder).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with just a folderId', () => {
|
||||
describe('when called with just a folderUid', () => {
|
||||
it('then it should call api to retrieve title', async () => {
|
||||
const getFolder = jest.fn().mockResolvedValue({ id: 0, title: 'Title from api' });
|
||||
const folder = await getInitialValues({ folderId: 0, getFolder });
|
||||
const getFolder = jest.fn().mockResolvedValue({ uid: '', title: 'Title from api' });
|
||||
const folder = await getInitialValues({ folderUid: '', getFolder });
|
||||
|
||||
expect(folder).toEqual({ label: 'Title from api', value: 0 });
|
||||
expect(folder).toEqual({ label: 'Title from api', value: '' });
|
||||
expect(getFolder).toHaveBeenCalledTimes(1);
|
||||
expect(getFolder).toHaveBeenCalledWith(0);
|
||||
expect(getFolder).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called without folderId', () => {
|
||||
describe('when called without folderUid', () => {
|
||||
it('then it should throw an error', async () => {
|
||||
const getFolder = jest.fn().mockResolvedValue({});
|
||||
await expect(getInitialValues({ getFolder })).rejects.toThrow();
|
||||
|
@ -9,7 +9,7 @@ import { useStyles2, ActionMeta, AsyncSelect, Input, InputActionMeta } from '@gr
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { createFolder, getFolderById, searchFolders } from 'app/features/manage-dashboards/state/actions';
|
||||
import { createFolder, getFolderByUid, searchFolders } from 'app/features/manage-dashboards/state/actions';
|
||||
import { DashboardSearchHit } from 'app/features/search/types';
|
||||
import { AccessControlAction, PermissionLevelString } from 'app/types';
|
||||
|
||||
@ -28,13 +28,13 @@ export interface CustomAdd {
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
onChange: ($folder: { title: string; id: number }) => void;
|
||||
onChange: ($folder: { title: string; uid: string }) => void;
|
||||
enableCreateNew?: boolean;
|
||||
rootName?: string;
|
||||
enableReset?: boolean;
|
||||
dashboardId?: number | string;
|
||||
initialTitle?: string;
|
||||
initialFolderId?: number;
|
||||
initialFolderUid?: string;
|
||||
permissionLevel?: Exclude<PermissionLevelString, PermissionLevelString.Admin>;
|
||||
filter?: FolderPickerFilter;
|
||||
allowEmpty?: boolean;
|
||||
@ -47,15 +47,15 @@ export interface Props {
|
||||
/**
|
||||
* Skips loading all folders in order to find the folder matching
|
||||
* the folder where the dashboard is stored.
|
||||
* Instead initialFolderId and initialTitle will be used to display the correct folder.
|
||||
* initialFolderId needs to have an value > -1 or an error will be thrown.
|
||||
* Instead initialFolderUid and initialTitle will be used to display the correct folder.
|
||||
* initialFolderUid needs to be a string or an error will be thrown.
|
||||
*/
|
||||
skipInitialLoad?: boolean;
|
||||
/** The id of the search input. Use this to set a matching label with htmlFor */
|
||||
inputId?: string;
|
||||
}
|
||||
export type SelectedFolder = SelectableValue<number>;
|
||||
const VALUE_FOR_ADD = -10;
|
||||
export type SelectedFolder = SelectableValue<string>;
|
||||
const VALUE_FOR_ADD = '-10';
|
||||
|
||||
export function FolderPicker(props: Props) {
|
||||
const {
|
||||
@ -67,7 +67,7 @@ export function FolderPicker(props: Props) {
|
||||
inputId,
|
||||
onClear,
|
||||
enableReset,
|
||||
initialFolderId,
|
||||
initialFolderUid,
|
||||
initialTitle = '',
|
||||
permissionLevel = PermissionLevelString.Edit,
|
||||
rootName = 'General',
|
||||
@ -90,14 +90,14 @@ export function FolderPicker(props: Props) {
|
||||
const getOptions = useCallback(
|
||||
async (query: string) => {
|
||||
const searchHits = await searchFolders(query, permissionLevel, accessControlMetadata);
|
||||
const options: Array<SelectableValue<number>> = mapSearchHitsToOptions(searchHits, filter);
|
||||
const options: Array<SelectableValue<string>> = mapSearchHitsToOptions(searchHits, filter);
|
||||
|
||||
const hasAccess =
|
||||
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor) ||
|
||||
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor);
|
||||
|
||||
if (hasAccess && rootName?.toLowerCase().startsWith(query.toLowerCase()) && showRoot) {
|
||||
options.unshift({ label: rootName, value: 0 });
|
||||
options.unshift({ label: rootName, value: '' });
|
||||
}
|
||||
|
||||
if (
|
||||
@ -106,7 +106,7 @@ export function FolderPicker(props: Props) {
|
||||
initialTitle !== '' &&
|
||||
!options.find((option) => option.label === initialTitle)
|
||||
) {
|
||||
options.unshift({ label: initialTitle, value: initialFolderId });
|
||||
options.unshift({ label: initialTitle, value: initialFolderUid });
|
||||
}
|
||||
if (enableCreateNew && Boolean(customAdd)) {
|
||||
return [...options, { value: VALUE_FOR_ADD, label: ADD_NEW_FOLER_OPTION, title: query }];
|
||||
@ -116,7 +116,7 @@ export function FolderPicker(props: Props) {
|
||||
},
|
||||
[
|
||||
enableReset,
|
||||
initialFolderId,
|
||||
initialFolderUid,
|
||||
initialTitle,
|
||||
permissionLevel,
|
||||
rootName,
|
||||
@ -133,19 +133,19 @@ export function FolderPicker(props: Props) {
|
||||
}, [getOptions]);
|
||||
|
||||
const loadInitialValue = async () => {
|
||||
const resetFolder: SelectableValue<number> = { label: initialTitle, value: undefined };
|
||||
const rootFolder: SelectableValue<number> = { label: rootName, value: 0 };
|
||||
const resetFolder: SelectableValue<string> = { label: initialTitle, value: undefined };
|
||||
const rootFolder: SelectableValue<string> = { label: rootName, value: '' };
|
||||
|
||||
const options = await getOptions('');
|
||||
|
||||
let folder: SelectableValue<number> | null = null;
|
||||
let folder: SelectableValue<string> | null = null;
|
||||
|
||||
if (initialFolderId !== undefined && initialFolderId !== null && initialFolderId > -1) {
|
||||
folder = options.find((option) => option.value === initialFolderId) || null;
|
||||
if (initialFolderUid !== undefined && initialFolderUid !== null) {
|
||||
folder = options.find((option) => option.value === initialFolderUid) || null;
|
||||
} else if (enableReset && initialTitle) {
|
||||
folder = resetFolder;
|
||||
} else if (initialFolderId) {
|
||||
folder = options.find((option) => option.id === initialFolderId) || null;
|
||||
} else if (initialFolderUid) {
|
||||
folder = options.find((option) => option.id === initialFolderUid) || null;
|
||||
}
|
||||
|
||||
if (!folder && !allowEmpty) {
|
||||
@ -166,25 +166,25 @@ export function FolderPicker(props: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
// if this is not the same as our initial value notify parent
|
||||
if (folder && folder.value !== initialFolderId) {
|
||||
!isCreatingNew && folder.value && folder.label && onChange({ id: folder.value, title: folder.label });
|
||||
if (folder && folder.value !== initialFolderUid) {
|
||||
!isCreatingNew && folder.value && folder.label && onChange({ uid: folder.value, title: folder.label });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [folder, initialFolderId]);
|
||||
}, [folder, initialFolderUid]);
|
||||
|
||||
// initial values for dropdown
|
||||
useAsync(async () => {
|
||||
if (skipInitialLoad) {
|
||||
const folder = await getInitialValues({
|
||||
getFolder: getFolderById,
|
||||
folderId: initialFolderId,
|
||||
getFolder: getFolderByUid,
|
||||
folderUid: initialFolderUid,
|
||||
folderName: initialTitle,
|
||||
});
|
||||
setFolder(folder);
|
||||
}
|
||||
|
||||
await loadInitialValue();
|
||||
}, [skipInitialLoad, initialFolderId, initialTitle]);
|
||||
}, [skipInitialLoad, initialFolderUid, initialTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (folder && folder.id === VALUE_FOR_ADD) {
|
||||
@ -193,7 +193,7 @@ export function FolderPicker(props: Props) {
|
||||
}, [folder]);
|
||||
|
||||
const onFolderChange = useCallback(
|
||||
(newFolder: SelectableValue<number> | null | undefined, actionMeta: ActionMeta) => {
|
||||
(newFolder: SelectableValue<string> | null | undefined, actionMeta: ActionMeta) => {
|
||||
if (newFolder?.value === VALUE_FOR_ADD) {
|
||||
setFolder({
|
||||
id: VALUE_FOR_ADD,
|
||||
@ -202,7 +202,7 @@ export function FolderPicker(props: Props) {
|
||||
setNewFolderValue(inputValue);
|
||||
} else {
|
||||
if (!newFolder) {
|
||||
newFolder = { value: 0, label: rootName };
|
||||
newFolder = { value: '', label: rootName };
|
||||
}
|
||||
|
||||
if (actionMeta.action === 'clear' && onClear) {
|
||||
@ -211,7 +211,7 @@ export function FolderPicker(props: Props) {
|
||||
}
|
||||
|
||||
setFolder(newFolder);
|
||||
onChange({ id: newFolder.value!, title: newFolder.label! });
|
||||
onChange({ uid: newFolder.value!, title: newFolder.label! });
|
||||
}
|
||||
},
|
||||
[onChange, onClear, rootName, inputValue]
|
||||
@ -223,11 +223,11 @@ export function FolderPicker(props: Props) {
|
||||
return false;
|
||||
}
|
||||
const newFolder = await createFolder({ title: folderName });
|
||||
let folder: SelectableValue<number> = { value: -1, label: 'Not created' };
|
||||
let folder: SelectableValue<string> = { value: '', label: 'Not created' };
|
||||
|
||||
if (newFolder.id > -1) {
|
||||
if (newFolder.uid) {
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
|
||||
folder = { value: newFolder.id, label: newFolder.title };
|
||||
folder = { value: newFolder.uid, label: newFolder.title };
|
||||
|
||||
setFolder(newFolder);
|
||||
onFolderChange(folder, { action: 'create-option', option: folder });
|
||||
@ -255,7 +255,7 @@ export function FolderPicker(props: Props) {
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
setFolder({ value: 0, label: rootName });
|
||||
setFolder({ value: '', label: rootName });
|
||||
setIsCreatingNew(false);
|
||||
}
|
||||
}
|
||||
@ -266,11 +266,11 @@ export function FolderPicker(props: Props) {
|
||||
const onNewFolderChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
const value = e.currentTarget.value;
|
||||
setNewFolderValue(value);
|
||||
setFolder({ id: -1, title: value });
|
||||
setFolder({ id: undefined, title: value });
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
setFolder({ value: 0, label: rootName });
|
||||
setFolder({ value: '', label: rootName });
|
||||
setIsCreatingNew(false);
|
||||
};
|
||||
|
||||
@ -344,25 +344,25 @@ export function FolderPicker(props: Props) {
|
||||
|
||||
function mapSearchHitsToOptions(hits: DashboardSearchHit[], filter?: FolderPickerFilter) {
|
||||
const filteredHits = filter ? filter(hits) : hits;
|
||||
return filteredHits.map((hit) => ({ label: hit.title, value: hit.id }));
|
||||
return filteredHits.map((hit) => ({ label: hit.title, value: hit.uid }));
|
||||
}
|
||||
interface Args {
|
||||
getFolder: typeof getFolderById;
|
||||
folderId?: number;
|
||||
getFolder: typeof getFolderByUid;
|
||||
folderUid?: string;
|
||||
folderName?: string;
|
||||
}
|
||||
|
||||
export async function getInitialValues({ folderName, folderId, getFolder }: Args): Promise<SelectableValue<number>> {
|
||||
if (folderId === null || folderId === undefined || folderId < 0) {
|
||||
throw new Error('folderId should to be greater or equal to zero.');
|
||||
export async function getInitialValues({ folderName, folderUid, getFolder }: Args): Promise<SelectableValue<string>> {
|
||||
if (folderUid === null || folderUid === undefined) {
|
||||
throw new Error('folderUid is not found.');
|
||||
}
|
||||
|
||||
if (folderName) {
|
||||
return { label: folderName, value: folderId };
|
||||
return { label: folderName, value: folderUid };
|
||||
}
|
||||
|
||||
const folderDto = await getFolder(folderId);
|
||||
return { label: folderDto.title, value: folderId };
|
||||
const folderDto = await getFolder(folderUid);
|
||||
return { label: folderDto.title, value: folderUid };
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
@ -11,7 +11,7 @@ import { FolderWarning, CustomAdd } from '../../../../../core/components/Select/
|
||||
|
||||
export interface Folder {
|
||||
title: string;
|
||||
id: number;
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
|
||||
@ -53,7 +53,7 @@ export function RuleFolderPicker(props: RuleFolderPickerProps) {
|
||||
showRoot={false}
|
||||
allowEmpty={true}
|
||||
initialTitle={value?.title}
|
||||
initialFolderId={value?.id}
|
||||
initialFolderUid={value?.uid}
|
||||
accessControlMetadata
|
||||
{...props}
|
||||
permissionLevel={PermissionLevelString.View}
|
||||
|
@ -76,7 +76,7 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
|
||||
showModal(SaveLibraryPanelModal, {
|
||||
isUnsavedPrompt: true,
|
||||
panel: dashboard.panelInEdit as PanelModelWithLibraryPanel,
|
||||
folderId: dashboard.meta.folderId as number,
|
||||
folderUid: dashboard.meta.folderUid ?? '',
|
||||
onConfirm: () => {
|
||||
hideModal();
|
||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
|
@ -30,8 +30,8 @@ export function GeneralSettingsUnconnected({
|
||||
}: Props): JSX.Element {
|
||||
const [renderCounter, setRenderCounter] = useState(0);
|
||||
|
||||
const onFolderChange = (folder: { id: number; title: string }) => {
|
||||
dashboard.meta.folderId = folder.id;
|
||||
const onFolderChange = (folder: { uid: string; title: string }) => {
|
||||
dashboard.meta.folderUid = folder.uid;
|
||||
dashboard.meta.folderTitle = folder.title;
|
||||
dashboard.meta.hasUnsavedFolderChange = true;
|
||||
};
|
||||
@ -109,7 +109,7 @@ export function GeneralSettingsUnconnected({
|
||||
<FolderPicker
|
||||
inputId="dashboard-folder-input"
|
||||
initialTitle={dashboard.meta.folderTitle}
|
||||
initialFolderId={dashboard.meta.folderId}
|
||||
initialFolderUid={dashboard.meta.folderUid}
|
||||
onChange={onFolderChange}
|
||||
enableCreateNew={true}
|
||||
dashboardId={dashboard.id}
|
||||
|
@ -498,7 +498,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
{this.state.showSaveLibraryPanelModal && (
|
||||
<SaveLibraryPanelModal
|
||||
panel={this.props.panel as PanelModelWithLibraryPanel}
|
||||
folderId={this.props.dashboard.meta.folderId as number}
|
||||
folderUid={this.props.dashboard.meta.folderUid ?? ''}
|
||||
onConfirm={this.onConfirmAndDismissLibarayPanelModel}
|
||||
onDiscard={this.onDiscard}
|
||||
onDismiss={this.onConfirmAndDismissLibarayPanelModel}
|
||||
|
@ -9,7 +9,7 @@ import { SaveDashboardFormProps } from '../types';
|
||||
|
||||
interface SaveDashboardAsFormDTO {
|
||||
title: string;
|
||||
$folder: { id?: number; title?: string };
|
||||
$folder: { uid?: string; title?: string };
|
||||
copyTags: boolean;
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
|
||||
const defaultValues: SaveDashboardAsFormDTO = {
|
||||
title: isNew ? dashboard.title : `${dashboard.title} Copy`,
|
||||
$folder: {
|
||||
id: dashboard.meta.folderId,
|
||||
uid: dashboard.meta.folderUid,
|
||||
title: dashboard.meta.folderTitle,
|
||||
},
|
||||
copyTags: false,
|
||||
@ -60,7 +60,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
|
||||
return 'Dashboard name cannot be the same as folder name';
|
||||
}
|
||||
try {
|
||||
await validationSrv.validateNewDashboardName(getFormValues().$folder.id, dashboardName);
|
||||
await validationSrv.validateNewDashboardName(getFormValues().$folder.uid, dashboardName);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return e instanceof Error ? e.message : 'Dashboard name is invalid';
|
||||
@ -84,7 +84,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
|
||||
const result = await onSubmit(
|
||||
clone,
|
||||
{
|
||||
folderId: data.$folder.id,
|
||||
folderUid: data.$folder.uid,
|
||||
},
|
||||
dashboard
|
||||
);
|
||||
@ -111,7 +111,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
|
||||
<FolderPicker
|
||||
{...field}
|
||||
dashboardId={dashboard.id}
|
||||
initialFolderId={dashboard.meta.folderId}
|
||||
initialFolderUid={dashboard.meta.folderUid}
|
||||
initialTitle={dashboard.meta.folderTitle}
|
||||
enableCreateNew
|
||||
/>
|
||||
|
@ -11,7 +11,7 @@ export interface SaveDashboardData {
|
||||
}
|
||||
|
||||
export interface SaveDashboardOptions extends CloneOptions {
|
||||
folderId?: number;
|
||||
folderUid?: string;
|
||||
overwrite?: boolean;
|
||||
message?: string;
|
||||
makeEditable?: boolean;
|
||||
@ -20,7 +20,7 @@ export interface SaveDashboardOptions extends CloneOptions {
|
||||
export interface SaveDashboardCommand {
|
||||
dashboard: DashboardDataDTO;
|
||||
message?: string;
|
||||
folderId?: number;
|
||||
folderUid?: string;
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
|
@ -15,12 +15,12 @@ import { DashboardSavedEvent } from 'app/types/events';
|
||||
import { SaveDashboardOptions } from './types';
|
||||
|
||||
const saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => {
|
||||
let folderId = options.folderId;
|
||||
if (folderId === undefined) {
|
||||
folderId = dashboard.meta.folderId ?? saveModel.folderId;
|
||||
let folderUid = options.folderUid;
|
||||
if (folderUid === undefined) {
|
||||
folderUid = dashboard.meta.folderUid ?? saveModel.folderUid;
|
||||
}
|
||||
|
||||
const result = await saveDashboardApiCall({ ...options, folderId, dashboard: saveModel });
|
||||
const result = await saveDashboardApiCall({ ...options, folderUid, dashboard: saveModel });
|
||||
// fetch updated access control permissions
|
||||
await contextSrv.fetchUserPermissions();
|
||||
return result;
|
||||
|
@ -7,10 +7,10 @@ import { AddLibraryPanelContents } from 'app/features/library-panels/components/
|
||||
import { ShareModalTabProps } from './types';
|
||||
|
||||
interface Props extends ShareModalTabProps {
|
||||
initialFolderId?: number;
|
||||
initialFolderUid?: string;
|
||||
}
|
||||
|
||||
export const ShareLibraryPanel = ({ panel, initialFolderId, onDismiss }: Props) => {
|
||||
export const ShareLibraryPanel = ({ panel, initialFolderUid, onDismiss }: Props) => {
|
||||
useEffect(() => {
|
||||
reportInteraction('grafana_dashboards_library_panel_share_viewed');
|
||||
}, []);
|
||||
@ -24,7 +24,7 @@ export const ShareLibraryPanel = ({ panel, initialFolderId, onDismiss }: Props)
|
||||
<p className="share-modal-info-text">
|
||||
<Trans i18nKey="share-modal.library.info">Create library panel.</Trans>
|
||||
</p>
|
||||
<AddLibraryPanelContents panel={panel} initialFolderId={initialFolderId} onDismiss={onDismiss!} />
|
||||
<AddLibraryPanelContents panel={panel} initialFolderUid={initialFolderUid} onDismiss={onDismiss!} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ export interface DashboardPageRouteParams {
|
||||
|
||||
export type DashboardPageRouteSearchParams = {
|
||||
tab?: string;
|
||||
folderId?: string;
|
||||
folderUid?: string;
|
||||
editPanel?: string;
|
||||
viewPanel?: string;
|
||||
editview?: string;
|
||||
@ -139,7 +139,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
urlSlug: match.params.slug,
|
||||
urlUid: match.params.uid,
|
||||
urlType: match.params.type,
|
||||
urlFolderId: queryParams.folderId,
|
||||
urlFolderUid: queryParams.folderUid,
|
||||
panelType: queryParams.panelType,
|
||||
routeName: this.props.route.routeName,
|
||||
fixUrl: !isPublic,
|
||||
|
@ -69,7 +69,7 @@ export class DashboardSrv {
|
||||
const parsedJson = JSON.parse(json);
|
||||
return saveDashboard({
|
||||
dashboard: parsedJson,
|
||||
folderId: this.dashboard?.meta.folderId || parsedJson.folderId,
|
||||
folderUid: this.dashboard?.meta.folderUid || parsedJson.folderUid,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,15 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { toStateKey } from 'app/features/variables/utils';
|
||||
import { DashboardDTO, DashboardInitPhase, DashboardRoutes, StoreState, ThunkDispatch, ThunkResult } from 'app/types';
|
||||
import {
|
||||
DashboardDTO,
|
||||
DashboardInitPhase,
|
||||
DashboardMeta,
|
||||
DashboardRoutes,
|
||||
StoreState,
|
||||
ThunkDispatch,
|
||||
ThunkResult,
|
||||
} from 'app/types';
|
||||
|
||||
import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
import { initVariablesTransaction } from '../../variables/state/actions';
|
||||
@ -27,7 +35,7 @@ export interface InitDashboardArgs {
|
||||
urlUid?: string;
|
||||
urlSlug?: string;
|
||||
urlType?: string;
|
||||
urlFolderId?: string;
|
||||
urlFolderUid?: string;
|
||||
panelType?: string;
|
||||
accessToken?: string;
|
||||
routeName?: string;
|
||||
@ -89,7 +97,7 @@ async function fetchDashboard(
|
||||
return dashDTO;
|
||||
}
|
||||
case DashboardRoutes.New: {
|
||||
return getNewDashboardModelData(args.urlFolderId, args.panelType);
|
||||
return getNewDashboardModelData(args.urlFolderUid, args.panelType);
|
||||
}
|
||||
case DashboardRoutes.Path: {
|
||||
const path = args.urlSlug ?? '';
|
||||
@ -255,14 +263,17 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewDashboardModelData(urlFolderId?: string, panelType?: string): any {
|
||||
export function getNewDashboardModelData(
|
||||
urlFolderUid?: string,
|
||||
panelType?: string
|
||||
): { dashboard: any; meta: DashboardMeta } {
|
||||
const data = {
|
||||
meta: {
|
||||
canStar: false,
|
||||
canShare: false,
|
||||
canDelete: false,
|
||||
isNew: true,
|
||||
folderId: 0,
|
||||
folderUid: '',
|
||||
},
|
||||
dashboard: {
|
||||
title: 'New dashboard',
|
||||
@ -276,8 +287,8 @@ export function getNewDashboardModelData(urlFolderId?: string, panelType?: strin
|
||||
},
|
||||
};
|
||||
|
||||
if (urlFolderId) {
|
||||
data.meta.folderId = parseInt(urlFolderId, 10);
|
||||
if (urlFolderUid) {
|
||||
data.meta.folderUid = urlFolderUid;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -75,7 +75,7 @@ export const addLibraryPanel = (dashboard: DashboardModel, panel: PanelModel) =>
|
||||
component: AddLibraryPanelModal,
|
||||
props: {
|
||||
panel,
|
||||
initialFolderId: dashboard.meta.folderId,
|
||||
initialFolderUid: dashboard.meta.folderUid,
|
||||
isOpen: true,
|
||||
},
|
||||
})
|
||||
|
@ -13,11 +13,11 @@ import { usePanelSave } from '../../utils/usePanelSave';
|
||||
interface AddLibraryPanelContentsProps {
|
||||
onDismiss: () => void;
|
||||
panel: PanelModel;
|
||||
initialFolderId?: number;
|
||||
initialFolderUid?: string;
|
||||
}
|
||||
|
||||
export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => {
|
||||
const [folderId, setFolderId] = useState(initialFolderId);
|
||||
export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: AddLibraryPanelContentsProps) => {
|
||||
const [folderUid, setFolderUid] = useState(initialFolderUid);
|
||||
const [panelName, setPanelName] = useState(panel.title);
|
||||
const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title);
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
@ -28,15 +28,15 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
|
||||
const { saveLibraryPanel } = usePanelSave();
|
||||
const onCreate = useCallback(() => {
|
||||
panel.libraryPanel = { uid: '', name: panelName };
|
||||
saveLibraryPanel(panel, folderId!).then((res) => {
|
||||
saveLibraryPanel(panel, folderUid!).then((res) => {
|
||||
if (!(res instanceof Error)) {
|
||||
onDismiss();
|
||||
}
|
||||
});
|
||||
}, [panel, panelName, folderId, onDismiss, saveLibraryPanel]);
|
||||
}, [panel, panelName, folderUid, onDismiss, saveLibraryPanel]);
|
||||
const isValidName = useAsync(async () => {
|
||||
try {
|
||||
return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderId === folderId);
|
||||
return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderUid === folderUid);
|
||||
} catch (err) {
|
||||
if (isFetchError(err)) {
|
||||
err.isHandled = true;
|
||||
@ -45,7 +45,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
|
||||
} finally {
|
||||
setWaiting(false);
|
||||
}
|
||||
}, [debouncedPanelName, folderId]);
|
||||
}, [debouncedPanelName, folderUid]);
|
||||
|
||||
const invalidInput =
|
||||
!isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting;
|
||||
@ -72,8 +72,8 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
|
||||
)}
|
||||
>
|
||||
<FolderPicker
|
||||
onChange={({ id }) => setFolderId(id)}
|
||||
initialFolderId={initialFolderId}
|
||||
onChange={({ uid }) => setFolderUid(uid)}
|
||||
initialFolderUid={initialFolderUid}
|
||||
inputId="share-panel-library-panel-folder-picker"
|
||||
/>
|
||||
</Field>
|
||||
@ -94,10 +94,10 @@ interface Props extends AddLibraryPanelContentsProps {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export const AddLibraryPanelModal = ({ isOpen = false, panel, initialFolderId, ...props }: Props) => {
|
||||
export const AddLibraryPanelModal = ({ isOpen = false, panel, initialFolderUid, ...props }: Props) => {
|
||||
return (
|
||||
<Modal title="Create library panel" isOpen={isOpen} onDismiss={props.onDismiss}>
|
||||
<AddLibraryPanelContents panel={panel} initialFolderId={initialFolderId} onDismiss={props.onDismiss} />
|
||||
<AddLibraryPanelContents panel={panel} initialFolderUid={initialFolderUid} onDismiss={props.onDismiss} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -28,9 +28,9 @@ jest.mock('debounce-promise', () => {
|
||||
const debounce = (fn: any) => {
|
||||
const debounced = () =>
|
||||
Promise.resolve([
|
||||
{ label: 'General', value: { id: 0, title: 'General' } },
|
||||
{ label: 'Folder1', value: { id: 1, title: 'Folder1' } },
|
||||
{ label: 'Folder2', value: { id: 2, title: 'Folder2' } },
|
||||
{ label: 'General', value: { uid: '', title: 'General' } },
|
||||
{ label: 'Folder1', value: { id: 'xMsQdBfWz', title: 'Folder1' } },
|
||||
{ label: 'Folder2', value: { id: 'wfTJJL5Wz', title: 'Folder2' } },
|
||||
]);
|
||||
return debounced;
|
||||
};
|
||||
@ -187,7 +187,7 @@ describe('LibraryPanelsSearch', () => {
|
||||
kind: LibraryElementKind.Panel,
|
||||
uid: 'uid',
|
||||
description: 'Library Panel Description',
|
||||
folderId: 0,
|
||||
folderUid: '',
|
||||
model: { type: 'timeseries', title: 'A title' },
|
||||
type: 'timeseries',
|
||||
orgId: 1,
|
||||
@ -242,7 +242,7 @@ describe('LibraryPanelsSearch', () => {
|
||||
kind: LibraryElementKind.Panel,
|
||||
uid: 'uid',
|
||||
description: 'Library Panel Description',
|
||||
folderId: 0,
|
||||
folderUid: '',
|
||||
model: { type: 'timeseries', title: 'A title' },
|
||||
type: 'timeseries',
|
||||
orgId: 1,
|
||||
@ -286,7 +286,7 @@ describe('LibraryPanelsSearch', () => {
|
||||
kind: LibraryElementKind.Panel,
|
||||
uid: 'uid',
|
||||
description: 'Library Panel Description',
|
||||
folderId: 0,
|
||||
folderUid: '',
|
||||
model: { type: 'timeseries', title: 'A title' },
|
||||
type: 'timeseries',
|
||||
orgId: 1,
|
||||
|
@ -106,7 +106,7 @@ function mockLibraryPanel({
|
||||
uid = '1',
|
||||
id = 1,
|
||||
orgId = 1,
|
||||
folderId = 0,
|
||||
folderUid = '',
|
||||
name = 'Test Panel',
|
||||
model = { type: 'text', title: 'Test Panel' },
|
||||
meta = {
|
||||
@ -126,7 +126,7 @@ function mockLibraryPanel({
|
||||
uid,
|
||||
id,
|
||||
orgId,
|
||||
folderId,
|
||||
folderUid,
|
||||
name,
|
||||
kind: LibraryElementKind.Panel,
|
||||
model,
|
||||
|
@ -70,7 +70,7 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
||||
<AddLibraryPanelModal
|
||||
panel={panel}
|
||||
onDismiss={() => setShowingAddPanelModal(false)}
|
||||
initialFolderId={dashboard?.meta.folderId}
|
||||
initialFolderUid={dashboard?.meta.folderUid}
|
||||
isOpen={showingAddPanelModal}
|
||||
/>
|
||||
)}
|
||||
|
@ -10,14 +10,21 @@ import { usePanelSave } from '../../utils/usePanelSave';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModelWithLibraryPanel;
|
||||
folderId: number;
|
||||
folderUid: string;
|
||||
isUnsavedPrompt?: boolean;
|
||||
onConfirm: () => void;
|
||||
onDismiss: () => void;
|
||||
onDiscard: () => void;
|
||||
}
|
||||
|
||||
export const SaveLibraryPanelModal = ({ panel, folderId, isUnsavedPrompt, onDismiss, onConfirm, onDiscard }: Props) => {
|
||||
export const SaveLibraryPanelModal = ({
|
||||
panel,
|
||||
folderUid,
|
||||
isUnsavedPrompt,
|
||||
onDismiss,
|
||||
onConfirm,
|
||||
onDiscard,
|
||||
}: Props) => {
|
||||
const [searchString, setSearchString] = useState('');
|
||||
const dashState = useAsync(async () => {
|
||||
const searchHits = await getConnectedDashboards(panel.libraryPanel.uid);
|
||||
@ -98,7 +105,7 @@ export const SaveLibraryPanelModal = ({ panel, folderId, isUnsavedPrompt, onDism
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveLibraryPanel(panel, folderId).then(() => {
|
||||
saveLibraryPanel(panel, folderUid).then(() => {
|
||||
onConfirm();
|
||||
});
|
||||
}}
|
||||
|
@ -77,10 +77,10 @@ export async function getLibraryPanelByName(name: string): Promise<LibraryElemen
|
||||
|
||||
export async function addLibraryPanel(
|
||||
panelSaveModel: PanelModelWithLibraryPanel,
|
||||
folderId: number
|
||||
folderUid: string
|
||||
): Promise<LibraryElementDTO> {
|
||||
const { result } = await getBackendSrv().post(`/api/library-elements`, {
|
||||
folderId,
|
||||
folderUid,
|
||||
name: panelSaveModel.libraryPanel.name,
|
||||
model: panelSaveModel,
|
||||
kind: LibraryElementKind.Panel,
|
||||
|
@ -32,7 +32,7 @@ export interface LibraryElementsSearchResult {
|
||||
export interface LibraryElementDTO {
|
||||
id: number;
|
||||
orgId: number;
|
||||
folderId: number;
|
||||
folderUid: string;
|
||||
uid: string;
|
||||
name: string;
|
||||
kind: LibraryElementKind;
|
||||
|
@ -13,9 +13,9 @@ export function createPanelLibrarySuccessNotification(message: string): AppNotif
|
||||
return createSuccessNotification(message);
|
||||
}
|
||||
|
||||
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderId: number): Promise<LibraryElementDTO> {
|
||||
export async function saveAndRefreshLibraryPanel(panel: PanelModel, folderUid: string): Promise<LibraryElementDTO> {
|
||||
const panelSaveModel = toPanelSaveModel(panel);
|
||||
const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId);
|
||||
const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderUid);
|
||||
updatePanelModelWithUpdate(panel, savedPanel);
|
||||
return savedPanel;
|
||||
}
|
||||
@ -44,13 +44,13 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDT
|
||||
panel.refresh();
|
||||
}
|
||||
|
||||
function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<LibraryElementDTO> {
|
||||
function saveOrUpdateLibraryPanel(panel: any, folderUid: string): Promise<LibraryElementDTO> {
|
||||
if (!panel.libraryPanel) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (panel.libraryPanel && panel.libraryPanel.uid === '') {
|
||||
return addLibraryPanel(panel, folderId!);
|
||||
return addLibraryPanel(panel, folderUid!);
|
||||
}
|
||||
|
||||
return updateLibraryPanel(panel);
|
||||
|
@ -15,9 +15,9 @@ import {
|
||||
|
||||
export const usePanelSave = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => {
|
||||
const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderUid: string) => {
|
||||
try {
|
||||
return await saveAndRefreshLibraryPanel(panel, folderId);
|
||||
return await saveAndRefreshLibraryPanel(panel, folderUid);
|
||||
} catch (err) {
|
||||
if (isFetchError(err)) {
|
||||
err.isHandled = true;
|
||||
|
@ -30,7 +30,7 @@ import { ImportDashboardLibraryPanelsList } from './ImportDashboardLibraryPanels
|
||||
interface Props extends Pick<FormAPI<ImportDashboardDTO>, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> {
|
||||
uidReset: boolean;
|
||||
inputs: DashboardInputs;
|
||||
initialFolderId: number;
|
||||
initialFolderUid: string;
|
||||
|
||||
onCancel: () => void;
|
||||
onUidReset: () => void;
|
||||
@ -44,7 +44,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
||||
getValues,
|
||||
uidReset,
|
||||
inputs,
|
||||
initialFolderId,
|
||||
initialFolderUid,
|
||||
onUidReset,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
@ -73,7 +73,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
||||
<Input
|
||||
{...register('title', {
|
||||
required: 'Name is required',
|
||||
validate: async (v: string) => await validateTitle(v, getValues().folder.id),
|
||||
validate: async (v: string) => await validateTitle(v, getValues().folder.uid),
|
||||
})}
|
||||
type="text"
|
||||
data-testid={selectors.components.ImportDashboardForm.name}
|
||||
@ -82,7 +82,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
||||
<Field label="Folder">
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<FolderPicker {...field} enableCreateNew initialFolderId={initialFolderId} />
|
||||
<FolderPicker {...field} enableCreateNew initialFolderUid={initialFolderUid} />
|
||||
)}
|
||||
name="folder"
|
||||
control={control}
|
||||
|
@ -21,7 +21,7 @@ const mapStateToProps = (state: StoreState) => {
|
||||
meta: state.importDashboard.meta,
|
||||
source: state.importDashboard.source,
|
||||
inputs: state.importDashboard.inputs,
|
||||
folder: searchObj.folderId ? { id: Number(searchObj.folderId) } : { id: 0 },
|
||||
folder: searchObj.folderUid ? { uid: String(searchObj.folderUid) } : { uid: '' },
|
||||
};
|
||||
};
|
||||
|
||||
@ -111,7 +111,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
||||
onUidReset={this.onUidReset}
|
||||
onSubmit={this.onSubmit}
|
||||
watch={watch}
|
||||
initialFolderId={folder.id}
|
||||
initialFolderUid={folder.uid}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
|
@ -17,8 +17,8 @@ class ValidationError extends Error {
|
||||
export class ValidationSrv {
|
||||
rootName = 'general';
|
||||
|
||||
validateNewDashboardName(folderId: any, name: string) {
|
||||
return this.validate(folderId, name, 'A dashboard or a folder with the same name already exists');
|
||||
validateNewDashboardName(folderUid: any, name: string) {
|
||||
return this.validate(folderUid, name, 'A dashboard or a folder with the same name already exists');
|
||||
}
|
||||
|
||||
validateNewFolderName(name?: string) {
|
||||
|
@ -24,7 +24,7 @@ describe('importDashboard', () => {
|
||||
],
|
||||
elements: [],
|
||||
folder: {
|
||||
id: 1,
|
||||
uid: '5v6e5VH4z',
|
||||
title: 'title',
|
||||
},
|
||||
};
|
||||
@ -64,7 +64,7 @@ describe('importDashboard', () => {
|
||||
title: 'Asda',
|
||||
uid: '12',
|
||||
},
|
||||
folderId: 1,
|
||||
folderUid: '5v6e5VH4z',
|
||||
inputs: [
|
||||
{
|
||||
name: 'ds-name',
|
||||
|
@ -166,7 +166,7 @@ export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkR
|
||||
dashboard: { ...dashboard, title: importDashboardForm.title, uid: importDashboardForm.uid || dashboard.uid },
|
||||
overwrite: true,
|
||||
inputs: inputsToPersist,
|
||||
folderId: importDashboardForm.folder.id,
|
||||
folderUid: importDashboardForm.folder.uid,
|
||||
});
|
||||
|
||||
const dashboardUrl = locationUtil.stripBaseFromUrl(result.importedUrl);
|
||||
@ -203,13 +203,16 @@ export function moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
|
||||
async function moveDashboard(uid: string, toFolder: FolderInfo) {
|
||||
const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${uid}`);
|
||||
|
||||
if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
|
||||
if (
|
||||
((fullDash.meta.folderUid === undefined || fullDash.meta.folderUid === null) && toFolder.uid === '') ||
|
||||
fullDash.meta.folderUid === toFolder.uid
|
||||
) {
|
||||
return { alreadyInFolder: true };
|
||||
}
|
||||
|
||||
const options = {
|
||||
dashboard: fullDash.dashboard,
|
||||
folderId: toFolder.id,
|
||||
folderUid: toFolder.uid,
|
||||
overwrite: false,
|
||||
};
|
||||
|
||||
@ -271,7 +274,7 @@ export function saveDashboard(options: SaveDashboardCommand) {
|
||||
dashboard: options.dashboard,
|
||||
message: options.message ?? '',
|
||||
overwrite: options.overwrite ?? false,
|
||||
folderId: options.folderId,
|
||||
folderUid: options.folderUid,
|
||||
});
|
||||
}
|
||||
|
||||
@ -296,6 +299,9 @@ export function searchFolders(
|
||||
});
|
||||
}
|
||||
|
||||
export function getFolderByUid(uid: string): Promise<{ uid: string; title: string }> {
|
||||
return getBackendSrv().get(`/api/folders/${uid}`);
|
||||
}
|
||||
export function getFolderById(id: number): Promise<{ id: number; title: string }> {
|
||||
return getBackendSrv().get(`/api/folders/id/${id}`);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export interface ImportDashboardDTO {
|
||||
constants: string[];
|
||||
dataSources: DataSourceInstanceSettings[];
|
||||
elements: LibraryElementDTO[];
|
||||
folder: { id: number; title?: string };
|
||||
folder: { uid: string; title?: string };
|
||||
}
|
||||
|
||||
export enum InputType {
|
||||
|
@ -29,9 +29,9 @@ export const validateGcomDashboard = (gcomDashboard: string) => {
|
||||
return match && (match[1] || match[2]) ? true : 'Could not find a valid Grafana.com ID';
|
||||
};
|
||||
|
||||
export const validateTitle = (newTitle: string, folderId: number) => {
|
||||
export const validateTitle = (newTitle: string, folderUid: string) => {
|
||||
return validationSrv
|
||||
.validateNewDashboardName(folderId, newTitle)
|
||||
.validateNewDashboardName(folderUid, newTitle)
|
||||
.then(() => {
|
||||
return true;
|
||||
})
|
||||
|
@ -3,17 +3,17 @@ import React, { FC } from 'react';
|
||||
import { Menu, Dropdown, Button, Icon } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
folderId?: number;
|
||||
folderUid?: string;
|
||||
canCreateFolders?: boolean;
|
||||
canCreateDashboards?: boolean;
|
||||
}
|
||||
|
||||
export const DashboardActions: FC<Props> = ({ folderId, canCreateFolders = false, canCreateDashboards = false }) => {
|
||||
export const DashboardActions: FC<Props> = ({ folderUid, canCreateFolders = false, canCreateDashboards = false }) => {
|
||||
const actionUrl = (type: string) => {
|
||||
let url = `dashboard/${type}`;
|
||||
|
||||
if (folderId) {
|
||||
url += `?folderId=${folderId}`;
|
||||
if (folderUid) {
|
||||
url += `?folderUid=${folderUid}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
@ -23,7 +23,7 @@ export const DashboardActions: FC<Props> = ({ folderId, canCreateFolders = false
|
||||
return (
|
||||
<Menu>
|
||||
{canCreateDashboards && <Menu.Item url={actionUrl('new')} label="New Dashboard" />}
|
||||
{!folderId && canCreateFolders && <Menu.Item url="dashboards/folder/new" label="New Folder" />}
|
||||
{!folderUid && canCreateFolders && <Menu.Item url="dashboards/folder/new" label="New Folder" />}
|
||||
{canCreateDashboards && <Menu.Item url={actionUrl('import')} label="Import" />}
|
||||
</Menu>
|
||||
);
|
||||
|
@ -24,14 +24,14 @@ export const ManageDashboardsNew = React.memo(({ folder }: Props) => {
|
||||
const { onKeyDown, keyboardEvents } = useKeyNavigationListener();
|
||||
|
||||
// TODO: we need to refactor DashboardActions to use folder.uid instead
|
||||
const folderId = folder?.id;
|
||||
// const folderUid = folder?.uid;
|
||||
|
||||
const folderUid = folder?.uid;
|
||||
const canSave = folder?.canSave;
|
||||
const { isEditor } = contextSrv;
|
||||
const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders;
|
||||
const canCreateFolders = contextSrv.hasAccess(AccessControlAction.FoldersCreate, isEditor);
|
||||
const canCreateDashboardsFallback = hasEditPermissionInFolders || !!canSave;
|
||||
const canCreateDashboards = folder?.id
|
||||
const canCreateDashboards = folderUid
|
||||
? contextSrv.hasAccessInMetadata(AccessControlAction.DashboardsCreate, folder, canCreateDashboardsFallback)
|
||||
: contextSrv.hasAccess(AccessControlAction.DashboardsCreate, canCreateDashboardsFallback);
|
||||
const viewActions = (folder === undefined && canCreateFolders) || canCreateDashboards;
|
||||
@ -55,7 +55,7 @@ export const ManageDashboardsNew = React.memo(({ folder }: Props) => {
|
||||
</div>
|
||||
{viewActions && (
|
||||
<DashboardActions
|
||||
folderId={folderId}
|
||||
folderUid={folderUid}
|
||||
canCreateFolders={canCreateFolders}
|
||||
canCreateDashboards={canCreateDashboards}
|
||||
/>
|
||||
|
@ -63,7 +63,7 @@ export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onD
|
||||
Move the {selectedDashboards.length} selected dashboard{selectedDashboards.length === 1 ? '' : 's'} to the
|
||||
following folder:
|
||||
</p>
|
||||
<FolderPicker onChange={(f) => setFolder(f as FolderInfo)} />
|
||||
<FolderPicker onChange={(f) => setFolder(f)} />
|
||||
</div>
|
||||
|
||||
<HorizontalGroup justify="center">
|
||||
|
@ -165,7 +165,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
||||
title="This folder doesn't have any dashboards yet"
|
||||
buttonIcon="plus"
|
||||
buttonTitle="Create Dashboard"
|
||||
buttonLink={`dashboard/new?folderId=${folderDTO.id}`}
|
||||
buttonLink={`dashboard/new?folderUid=${folderDTO.uid}`}
|
||||
proTip="Add/move dashboards to your folder at ->"
|
||||
proTipLink="dashboards"
|
||||
proTipLinkTitle="Manage dashboards"
|
||||
|
@ -256,7 +256,7 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
|
||||
showRoot={false}
|
||||
allowEmpty={true}
|
||||
initialTitle={props.value?.title}
|
||||
initialFolderId={props.value?.id}
|
||||
initialFolderUid={props.value?.uid}
|
||||
permissionLevel={PermissionLevelString.View}
|
||||
onClear={() => props.onChange('')}
|
||||
{...props}
|
||||
|
@ -31,7 +31,7 @@ export interface FolderInfo {
|
||||
/**
|
||||
* @deprecated use uid instead.
|
||||
*/
|
||||
id?: number;
|
||||
id?: number; // can't be totally removed as search and alerts api aren't supporting folderUids yet. It will break DashList and AlertList panel
|
||||
uid?: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
|
Loading…
Reference in New Issue
Block a user