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:
Leo 2022-11-17 09:22:57 +01:00 committed by GitHub
parent ab36252c86
commit 27b6b3b3bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 194 additions and 173 deletions

View File

@ -3227,12 +3227,11 @@ exports[`better eslint`] = {
], ],
"public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx:5381": [ "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.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"]
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
], ],
"public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx:5381": [ "public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [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": [ "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.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [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.", "2"]
[0, 0, 0, "Do not use any type assertions.", "3"]
], ],
"public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx:5381": [ "public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],
"public/app/features/search/page/components/MoveToFolderModal.tsx:5381": [ "public/app/features/search/page/components/MoveToFolderModal.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Do not use any type assertions.", "1"]
], ],
"public/app/features/search/page/components/SearchResultsCards.tsx:5381": [ "public/app/features/search/page/components/SearchResultsCards.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -16,8 +16,8 @@ describe('FolderPicker', () => {
jest jest
.spyOn(api, 'searchFolders') .spyOn(api, 'searchFolders')
.mockResolvedValue([ .mockResolvedValue([
{ title: 'Dash 1', id: 1 } as DashboardSearchHit, { title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
{ title: 'Dash 2', id: 2 } as DashboardSearchHit, { title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
]); ]);
render(<FolderPicker onChange={jest.fn()} />); render(<FolderPicker onChange={jest.fn()} />);
@ -28,12 +28,12 @@ describe('FolderPicker', () => {
jest jest
.spyOn(api, 'searchFolders') .spyOn(api, 'searchFolders')
.mockResolvedValue([ .mockResolvedValue([
{ title: 'Dash 1', id: 1 } as DashboardSearchHit, { title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
{ title: 'Dash 2', id: 2 } as DashboardSearchHit, { title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
{ title: 'Dash 3', id: 3 } 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); const pickerContainer = screen.getByLabelText(selectors.components.FolderPicker.input);
selectEvent.openMenu(pickerContainer); selectEvent.openMenu(pickerContainer);
@ -46,13 +46,13 @@ describe('FolderPicker', () => {
}); });
it('should allow creating a new option', async () => { 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 jest
.spyOn(api, 'searchFolders') .spyOn(api, 'searchFolders')
.mockResolvedValue([ .mockResolvedValue([
{ title: 'Dash 1', id: 1 } as DashboardSearchHit, { title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
{ title: 'Dash 2', id: 2 } as DashboardSearchHit, { title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
]); ]);
const onChangeFn = jest.fn(); const onChangeFn = jest.fn();
@ -70,7 +70,7 @@ describe('FolderPicker', () => {
expect(create).toHaveBeenCalledWith({ title: newFolder.title }); 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(() => { await waitFor(() => {
expect(screen.getByText(newFolder.title)).toBeInTheDocument(); expect(screen.getByText(newFolder.title)).toBeInTheDocument();
}); });
@ -80,8 +80,8 @@ describe('FolderPicker', () => {
jest jest
.spyOn(api, 'searchFolders') .spyOn(api, 'searchFolders')
.mockResolvedValue([ .mockResolvedValue([
{ title: 'Dash 1', id: 1 } as DashboardSearchHit, { title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
{ title: 'Dash 2', id: 2 } as DashboardSearchHit, { title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
]); ]);
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
@ -101,8 +101,8 @@ describe('FolderPicker', () => {
jest jest
.spyOn(api, 'searchFolders') .spyOn(api, 'searchFolders')
.mockResolvedValue([ .mockResolvedValue([
{ title: 'Dash 1', id: 1 } as DashboardSearchHit, { title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
{ title: 'Dash 2', id: 2 } as DashboardSearchHit, { title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
]); ]);
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
@ -122,8 +122,8 @@ describe('FolderPicker', () => {
jest jest
.spyOn(api, 'searchFolders') .spyOn(api, 'searchFolders')
.mockResolvedValue([ .mockResolvedValue([
{ title: 'Dash 1', id: 1 } as DashboardSearchHit, { title: 'Dash 1', uid: 'xMsQdBfWz' } as DashboardSearchHit,
{ title: 'Dash 2', id: 2 } as DashboardSearchHit, { title: 'Dash 2', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
]); ]);
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
@ -141,28 +141,28 @@ describe('FolderPicker', () => {
}); });
describe('getInitialValues', () => { describe('getInitialValues', () => {
describe('when called with folderId and title', () => { describe('when called with folderUid and title', () => {
it('then it should return folderId and title', async () => { it('then it should return folderUid and title', async () => {
const getFolder = jest.fn().mockResolvedValue({}); 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(); 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 () => { it('then it should call api to retrieve title', async () => {
const getFolder = jest.fn().mockResolvedValue({ id: 0, title: 'Title from api' }); const getFolder = jest.fn().mockResolvedValue({ uid: '', title: 'Title from api' });
const folder = await getInitialValues({ folderId: 0, getFolder }); 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).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 () => { it('then it should throw an error', async () => {
const getFolder = jest.fn().mockResolvedValue({}); const getFolder = jest.fn().mockResolvedValue({});
await expect(getInitialValues({ getFolder })).rejects.toThrow(); await expect(getInitialValues({ getFolder })).rejects.toThrow();

View File

@ -9,7 +9,7 @@ import { useStyles2, ActionMeta, AsyncSelect, Input, InputActionMeta } from '@gr
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv'; 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 { DashboardSearchHit } from 'app/features/search/types';
import { AccessControlAction, PermissionLevelString } from 'app/types'; import { AccessControlAction, PermissionLevelString } from 'app/types';
@ -28,13 +28,13 @@ export interface CustomAdd {
} }
export interface Props { export interface Props {
onChange: ($folder: { title: string; id: number }) => void; onChange: ($folder: { title: string; uid: string }) => void;
enableCreateNew?: boolean; enableCreateNew?: boolean;
rootName?: string; rootName?: string;
enableReset?: boolean; enableReset?: boolean;
dashboardId?: number | string; dashboardId?: number | string;
initialTitle?: string; initialTitle?: string;
initialFolderId?: number; initialFolderUid?: string;
permissionLevel?: Exclude<PermissionLevelString, PermissionLevelString.Admin>; permissionLevel?: Exclude<PermissionLevelString, PermissionLevelString.Admin>;
filter?: FolderPickerFilter; filter?: FolderPickerFilter;
allowEmpty?: boolean; allowEmpty?: boolean;
@ -47,15 +47,15 @@ export interface Props {
/** /**
* Skips loading all folders in order to find the folder matching * Skips loading all folders in order to find the folder matching
* the folder where the dashboard is stored. * the folder where the dashboard is stored.
* Instead initialFolderId and initialTitle will be used to display the correct folder. * Instead initialFolderUid and initialTitle will be used to display the correct folder.
* initialFolderId needs to have an value > -1 or an error will be thrown. * initialFolderUid needs to be a string or an error will be thrown.
*/ */
skipInitialLoad?: boolean; skipInitialLoad?: boolean;
/** The id of the search input. Use this to set a matching label with htmlFor */ /** The id of the search input. Use this to set a matching label with htmlFor */
inputId?: string; inputId?: string;
} }
export type SelectedFolder = SelectableValue<number>; export type SelectedFolder = SelectableValue<string>;
const VALUE_FOR_ADD = -10; const VALUE_FOR_ADD = '-10';
export function FolderPicker(props: Props) { export function FolderPicker(props: Props) {
const { const {
@ -67,7 +67,7 @@ export function FolderPicker(props: Props) {
inputId, inputId,
onClear, onClear,
enableReset, enableReset,
initialFolderId, initialFolderUid,
initialTitle = '', initialTitle = '',
permissionLevel = PermissionLevelString.Edit, permissionLevel = PermissionLevelString.Edit,
rootName = 'General', rootName = 'General',
@ -90,14 +90,14 @@ export function FolderPicker(props: Props) {
const getOptions = useCallback( const getOptions = useCallback(
async (query: string) => { async (query: string) => {
const searchHits = await searchFolders(query, permissionLevel, accessControlMetadata); 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 = const hasAccess =
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor) || contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor) ||
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor); contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor);
if (hasAccess && rootName?.toLowerCase().startsWith(query.toLowerCase()) && showRoot) { if (hasAccess && rootName?.toLowerCase().startsWith(query.toLowerCase()) && showRoot) {
options.unshift({ label: rootName, value: 0 }); options.unshift({ label: rootName, value: '' });
} }
if ( if (
@ -106,7 +106,7 @@ export function FolderPicker(props: Props) {
initialTitle !== '' && initialTitle !== '' &&
!options.find((option) => option.label === initialTitle) !options.find((option) => option.label === initialTitle)
) { ) {
options.unshift({ label: initialTitle, value: initialFolderId }); options.unshift({ label: initialTitle, value: initialFolderUid });
} }
if (enableCreateNew && Boolean(customAdd)) { if (enableCreateNew && Boolean(customAdd)) {
return [...options, { value: VALUE_FOR_ADD, label: ADD_NEW_FOLER_OPTION, title: query }]; return [...options, { value: VALUE_FOR_ADD, label: ADD_NEW_FOLER_OPTION, title: query }];
@ -116,7 +116,7 @@ export function FolderPicker(props: Props) {
}, },
[ [
enableReset, enableReset,
initialFolderId, initialFolderUid,
initialTitle, initialTitle,
permissionLevel, permissionLevel,
rootName, rootName,
@ -133,19 +133,19 @@ export function FolderPicker(props: Props) {
}, [getOptions]); }, [getOptions]);
const loadInitialValue = async () => { const loadInitialValue = async () => {
const resetFolder: SelectableValue<number> = { label: initialTitle, value: undefined }; const resetFolder: SelectableValue<string> = { label: initialTitle, value: undefined };
const rootFolder: SelectableValue<number> = { label: rootName, value: 0 }; const rootFolder: SelectableValue<string> = { label: rootName, value: '' };
const options = await getOptions(''); const options = await getOptions('');
let folder: SelectableValue<number> | null = null; let folder: SelectableValue<string> | null = null;
if (initialFolderId !== undefined && initialFolderId !== null && initialFolderId > -1) { if (initialFolderUid !== undefined && initialFolderUid !== null) {
folder = options.find((option) => option.value === initialFolderId) || null; folder = options.find((option) => option.value === initialFolderUid) || null;
} else if (enableReset && initialTitle) { } else if (enableReset && initialTitle) {
folder = resetFolder; folder = resetFolder;
} else if (initialFolderId) { } else if (initialFolderUid) {
folder = options.find((option) => option.id === initialFolderId) || null; folder = options.find((option) => option.id === initialFolderUid) || null;
} }
if (!folder && !allowEmpty) { if (!folder && !allowEmpty) {
@ -166,25 +166,25 @@ export function FolderPicker(props: Props) {
useEffect(() => { useEffect(() => {
// if this is not the same as our initial value notify parent // if this is not the same as our initial value notify parent
if (folder && folder.value !== initialFolderId) { if (folder && folder.value !== initialFolderUid) {
!isCreatingNew && folder.value && folder.label && onChange({ id: folder.value, title: folder.label }); !isCreatingNew && folder.value && folder.label && onChange({ uid: folder.value, title: folder.label });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [folder, initialFolderId]); }, [folder, initialFolderUid]);
// initial values for dropdown // initial values for dropdown
useAsync(async () => { useAsync(async () => {
if (skipInitialLoad) { if (skipInitialLoad) {
const folder = await getInitialValues({ const folder = await getInitialValues({
getFolder: getFolderById, getFolder: getFolderByUid,
folderId: initialFolderId, folderUid: initialFolderUid,
folderName: initialTitle, folderName: initialTitle,
}); });
setFolder(folder); setFolder(folder);
} }
await loadInitialValue(); await loadInitialValue();
}, [skipInitialLoad, initialFolderId, initialTitle]); }, [skipInitialLoad, initialFolderUid, initialTitle]);
useEffect(() => { useEffect(() => {
if (folder && folder.id === VALUE_FOR_ADD) { if (folder && folder.id === VALUE_FOR_ADD) {
@ -193,7 +193,7 @@ export function FolderPicker(props: Props) {
}, [folder]); }, [folder]);
const onFolderChange = useCallback( const onFolderChange = useCallback(
(newFolder: SelectableValue<number> | null | undefined, actionMeta: ActionMeta) => { (newFolder: SelectableValue<string> | null | undefined, actionMeta: ActionMeta) => {
if (newFolder?.value === VALUE_FOR_ADD) { if (newFolder?.value === VALUE_FOR_ADD) {
setFolder({ setFolder({
id: VALUE_FOR_ADD, id: VALUE_FOR_ADD,
@ -202,7 +202,7 @@ export function FolderPicker(props: Props) {
setNewFolderValue(inputValue); setNewFolderValue(inputValue);
} else { } else {
if (!newFolder) { if (!newFolder) {
newFolder = { value: 0, label: rootName }; newFolder = { value: '', label: rootName };
} }
if (actionMeta.action === 'clear' && onClear) { if (actionMeta.action === 'clear' && onClear) {
@ -211,7 +211,7 @@ export function FolderPicker(props: Props) {
} }
setFolder(newFolder); setFolder(newFolder);
onChange({ id: newFolder.value!, title: newFolder.label! }); onChange({ uid: newFolder.value!, title: newFolder.label! });
} }
}, },
[onChange, onClear, rootName, inputValue] [onChange, onClear, rootName, inputValue]
@ -223,11 +223,11 @@ export function FolderPicker(props: Props) {
return false; return false;
} }
const newFolder = await createFolder({ title: folderName }); 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']); appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
folder = { value: newFolder.id, label: newFolder.title }; folder = { value: newFolder.uid, label: newFolder.title };
setFolder(newFolder); setFolder(newFolder);
onFolderChange(folder, { action: 'create-option', option: folder }); onFolderChange(folder, { action: 'create-option', option: folder });
@ -255,7 +255,7 @@ export function FolderPicker(props: Props) {
break; break;
} }
case 'Escape': { case 'Escape': {
setFolder({ value: 0, label: rootName }); setFolder({ value: '', label: rootName });
setIsCreatingNew(false); setIsCreatingNew(false);
} }
} }
@ -266,11 +266,11 @@ export function FolderPicker(props: Props) {
const onNewFolderChange = (e: FormEvent<HTMLInputElement>) => { const onNewFolderChange = (e: FormEvent<HTMLInputElement>) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
setNewFolderValue(value); setNewFolderValue(value);
setFolder({ id: -1, title: value }); setFolder({ id: undefined, title: value });
}; };
const onBlur = () => { const onBlur = () => {
setFolder({ value: 0, label: rootName }); setFolder({ value: '', label: rootName });
setIsCreatingNew(false); setIsCreatingNew(false);
}; };
@ -344,25 +344,25 @@ export function FolderPicker(props: Props) {
function mapSearchHitsToOptions(hits: DashboardSearchHit[], filter?: FolderPickerFilter) { function mapSearchHitsToOptions(hits: DashboardSearchHit[], filter?: FolderPickerFilter) {
const filteredHits = filter ? filter(hits) : hits; 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 { interface Args {
getFolder: typeof getFolderById; getFolder: typeof getFolderByUid;
folderId?: number; folderUid?: string;
folderName?: string; folderName?: string;
} }
export async function getInitialValues({ folderName, folderId, getFolder }: Args): Promise<SelectableValue<number>> { export async function getInitialValues({ folderName, folderUid, getFolder }: Args): Promise<SelectableValue<string>> {
if (folderId === null || folderId === undefined || folderId < 0) { if (folderUid === null || folderUid === undefined) {
throw new Error('folderId should to be greater or equal to zero.'); throw new Error('folderUid is not found.');
} }
if (folderName) { if (folderName) {
return { label: folderName, value: folderId }; return { label: folderName, value: folderUid };
} }
const folderDto = await getFolder(folderId); const folderDto = await getFolder(folderUid);
return { label: folderDto.title, value: folderId }; return { label: folderDto.title, value: folderUid };
} }
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({

View File

@ -11,7 +11,7 @@ import { FolderWarning, CustomAdd } from '../../../../../core/components/Select/
export interface Folder { export interface Folder {
title: string; title: string;
id: number; uid: string;
} }
export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> { export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
@ -53,7 +53,7 @@ export function RuleFolderPicker(props: RuleFolderPickerProps) {
showRoot={false} showRoot={false}
allowEmpty={true} allowEmpty={true}
initialTitle={value?.title} initialTitle={value?.title}
initialFolderId={value?.id} initialFolderUid={value?.uid}
accessControlMetadata accessControlMetadata
{...props} {...props}
permissionLevel={PermissionLevelString.View} permissionLevel={PermissionLevelString.View}

View File

@ -76,7 +76,7 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
showModal(SaveLibraryPanelModal, { showModal(SaveLibraryPanelModal, {
isUnsavedPrompt: true, isUnsavedPrompt: true,
panel: dashboard.panelInEdit as PanelModelWithLibraryPanel, panel: dashboard.panelInEdit as PanelModelWithLibraryPanel,
folderId: dashboard.meta.folderId as number, folderUid: dashboard.meta.folderUid ?? '',
onConfirm: () => { onConfirm: () => {
hideModal(); hideModal();
moveToBlockedLocationAfterReactStateUpdate(location); moveToBlockedLocationAfterReactStateUpdate(location);

View File

@ -30,8 +30,8 @@ export function GeneralSettingsUnconnected({
}: Props): JSX.Element { }: Props): JSX.Element {
const [renderCounter, setRenderCounter] = useState(0); const [renderCounter, setRenderCounter] = useState(0);
const onFolderChange = (folder: { id: number; title: string }) => { const onFolderChange = (folder: { uid: string; title: string }) => {
dashboard.meta.folderId = folder.id; dashboard.meta.folderUid = folder.uid;
dashboard.meta.folderTitle = folder.title; dashboard.meta.folderTitle = folder.title;
dashboard.meta.hasUnsavedFolderChange = true; dashboard.meta.hasUnsavedFolderChange = true;
}; };
@ -109,7 +109,7 @@ export function GeneralSettingsUnconnected({
<FolderPicker <FolderPicker
inputId="dashboard-folder-input" inputId="dashboard-folder-input"
initialTitle={dashboard.meta.folderTitle} initialTitle={dashboard.meta.folderTitle}
initialFolderId={dashboard.meta.folderId} initialFolderUid={dashboard.meta.folderUid}
onChange={onFolderChange} onChange={onFolderChange}
enableCreateNew={true} enableCreateNew={true}
dashboardId={dashboard.id} dashboardId={dashboard.id}

View File

@ -498,7 +498,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
{this.state.showSaveLibraryPanelModal && ( {this.state.showSaveLibraryPanelModal && (
<SaveLibraryPanelModal <SaveLibraryPanelModal
panel={this.props.panel as PanelModelWithLibraryPanel} panel={this.props.panel as PanelModelWithLibraryPanel}
folderId={this.props.dashboard.meta.folderId as number} folderUid={this.props.dashboard.meta.folderUid ?? ''}
onConfirm={this.onConfirmAndDismissLibarayPanelModel} onConfirm={this.onConfirmAndDismissLibarayPanelModel}
onDiscard={this.onDiscard} onDiscard={this.onDiscard}
onDismiss={this.onConfirmAndDismissLibarayPanelModel} onDismiss={this.onConfirmAndDismissLibarayPanelModel}

View File

@ -9,7 +9,7 @@ import { SaveDashboardFormProps } from '../types';
interface SaveDashboardAsFormDTO { interface SaveDashboardAsFormDTO {
title: string; title: string;
$folder: { id?: number; title?: string }; $folder: { uid?: string; title?: string };
copyTags: boolean; copyTags: boolean;
} }
@ -49,7 +49,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
const defaultValues: SaveDashboardAsFormDTO = { const defaultValues: SaveDashboardAsFormDTO = {
title: isNew ? dashboard.title : `${dashboard.title} Copy`, title: isNew ? dashboard.title : `${dashboard.title} Copy`,
$folder: { $folder: {
id: dashboard.meta.folderId, uid: dashboard.meta.folderUid,
title: dashboard.meta.folderTitle, title: dashboard.meta.folderTitle,
}, },
copyTags: false, copyTags: false,
@ -60,7 +60,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
return 'Dashboard name cannot be the same as folder name'; return 'Dashboard name cannot be the same as folder name';
} }
try { try {
await validationSrv.validateNewDashboardName(getFormValues().$folder.id, dashboardName); await validationSrv.validateNewDashboardName(getFormValues().$folder.uid, dashboardName);
return true; return true;
} catch (e) { } catch (e) {
return e instanceof Error ? e.message : 'Dashboard name is invalid'; return e instanceof Error ? e.message : 'Dashboard name is invalid';
@ -84,7 +84,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
const result = await onSubmit( const result = await onSubmit(
clone, clone,
{ {
folderId: data.$folder.id, folderUid: data.$folder.uid,
}, },
dashboard dashboard
); );
@ -111,7 +111,7 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
<FolderPicker <FolderPicker
{...field} {...field}
dashboardId={dashboard.id} dashboardId={dashboard.id}
initialFolderId={dashboard.meta.folderId} initialFolderUid={dashboard.meta.folderUid}
initialTitle={dashboard.meta.folderTitle} initialTitle={dashboard.meta.folderTitle}
enableCreateNew enableCreateNew
/> />

View File

@ -11,7 +11,7 @@ export interface SaveDashboardData {
} }
export interface SaveDashboardOptions extends CloneOptions { export interface SaveDashboardOptions extends CloneOptions {
folderId?: number; folderUid?: string;
overwrite?: boolean; overwrite?: boolean;
message?: string; message?: string;
makeEditable?: boolean; makeEditable?: boolean;
@ -20,7 +20,7 @@ export interface SaveDashboardOptions extends CloneOptions {
export interface SaveDashboardCommand { export interface SaveDashboardCommand {
dashboard: DashboardDataDTO; dashboard: DashboardDataDTO;
message?: string; message?: string;
folderId?: number; folderUid?: string;
overwrite?: boolean; overwrite?: boolean;
} }

View File

@ -15,12 +15,12 @@ import { DashboardSavedEvent } from 'app/types/events';
import { SaveDashboardOptions } from './types'; import { SaveDashboardOptions } from './types';
const saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => { const saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dashboard: DashboardModel) => {
let folderId = options.folderId; let folderUid = options.folderUid;
if (folderId === undefined) { if (folderUid === undefined) {
folderId = dashboard.meta.folderId ?? saveModel.folderId; 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 // fetch updated access control permissions
await contextSrv.fetchUserPermissions(); await contextSrv.fetchUserPermissions();
return result; return result;

View File

@ -7,10 +7,10 @@ import { AddLibraryPanelContents } from 'app/features/library-panels/components/
import { ShareModalTabProps } from './types'; import { ShareModalTabProps } from './types';
interface Props extends ShareModalTabProps { interface Props extends ShareModalTabProps {
initialFolderId?: number; initialFolderUid?: string;
} }
export const ShareLibraryPanel = ({ panel, initialFolderId, onDismiss }: Props) => { export const ShareLibraryPanel = ({ panel, initialFolderUid, onDismiss }: Props) => {
useEffect(() => { useEffect(() => {
reportInteraction('grafana_dashboards_library_panel_share_viewed'); reportInteraction('grafana_dashboards_library_panel_share_viewed');
}, []); }, []);
@ -24,7 +24,7 @@ export const ShareLibraryPanel = ({ panel, initialFolderId, onDismiss }: Props)
<p className="share-modal-info-text"> <p className="share-modal-info-text">
<Trans i18nKey="share-modal.library.info">Create library panel.</Trans> <Trans i18nKey="share-modal.library.info">Create library panel.</Trans>
</p> </p>
<AddLibraryPanelContents panel={panel} initialFolderId={initialFolderId} onDismiss={onDismiss!} /> <AddLibraryPanelContents panel={panel} initialFolderUid={initialFolderUid} onDismiss={onDismiss!} />
</> </>
); );
}; };

View File

@ -45,7 +45,7 @@ export interface DashboardPageRouteParams {
export type DashboardPageRouteSearchParams = { export type DashboardPageRouteSearchParams = {
tab?: string; tab?: string;
folderId?: string; folderUid?: string;
editPanel?: string; editPanel?: string;
viewPanel?: string; viewPanel?: string;
editview?: string; editview?: string;
@ -139,7 +139,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
urlSlug: match.params.slug, urlSlug: match.params.slug,
urlUid: match.params.uid, urlUid: match.params.uid,
urlType: match.params.type, urlType: match.params.type,
urlFolderId: queryParams.folderId, urlFolderUid: queryParams.folderUid,
panelType: queryParams.panelType, panelType: queryParams.panelType,
routeName: this.props.route.routeName, routeName: this.props.route.routeName,
fixUrl: !isPublic, fixUrl: !isPublic,

View File

@ -69,7 +69,7 @@ export class DashboardSrv {
const parsedJson = JSON.parse(json); const parsedJson = JSON.parse(json);
return saveDashboard({ return saveDashboard({
dashboard: parsedJson, dashboard: parsedJson,
folderId: this.dashboard?.meta.folderId || parsedJson.folderId, folderUid: this.dashboard?.meta.folderUid || parsedJson.folderUid,
}); });
} }

View File

@ -12,7 +12,15 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { toStateKey } from 'app/features/variables/utils'; 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 { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
import { initVariablesTransaction } from '../../variables/state/actions'; import { initVariablesTransaction } from '../../variables/state/actions';
@ -27,7 +35,7 @@ export interface InitDashboardArgs {
urlUid?: string; urlUid?: string;
urlSlug?: string; urlSlug?: string;
urlType?: string; urlType?: string;
urlFolderId?: string; urlFolderUid?: string;
panelType?: string; panelType?: string;
accessToken?: string; accessToken?: string;
routeName?: string; routeName?: string;
@ -89,7 +97,7 @@ async function fetchDashboard(
return dashDTO; return dashDTO;
} }
case DashboardRoutes.New: { case DashboardRoutes.New: {
return getNewDashboardModelData(args.urlFolderId, args.panelType); return getNewDashboardModelData(args.urlFolderUid, args.panelType);
} }
case DashboardRoutes.Path: { case DashboardRoutes.Path: {
const path = args.urlSlug ?? ''; 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 = { const data = {
meta: { meta: {
canStar: false, canStar: false,
canShare: false, canShare: false,
canDelete: false, canDelete: false,
isNew: true, isNew: true,
folderId: 0, folderUid: '',
}, },
dashboard: { dashboard: {
title: 'New dashboard', title: 'New dashboard',
@ -276,8 +287,8 @@ export function getNewDashboardModelData(urlFolderId?: string, panelType?: strin
}, },
}; };
if (urlFolderId) { if (urlFolderUid) {
data.meta.folderId = parseInt(urlFolderId, 10); data.meta.folderUid = urlFolderUid;
} }
return data; return data;

View File

@ -75,7 +75,7 @@ export const addLibraryPanel = (dashboard: DashboardModel, panel: PanelModel) =>
component: AddLibraryPanelModal, component: AddLibraryPanelModal,
props: { props: {
panel, panel,
initialFolderId: dashboard.meta.folderId, initialFolderUid: dashboard.meta.folderUid,
isOpen: true, isOpen: true,
}, },
}) })

View File

@ -13,11 +13,11 @@ import { usePanelSave } from '../../utils/usePanelSave';
interface AddLibraryPanelContentsProps { interface AddLibraryPanelContentsProps {
onDismiss: () => void; onDismiss: () => void;
panel: PanelModel; panel: PanelModel;
initialFolderId?: number; initialFolderUid?: string;
} }
export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => { export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }: AddLibraryPanelContentsProps) => {
const [folderId, setFolderId] = useState(initialFolderId); const [folderUid, setFolderUid] = useState(initialFolderUid);
const [panelName, setPanelName] = useState(panel.title); const [panelName, setPanelName] = useState(panel.title);
const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title); const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title);
const [waiting, setWaiting] = useState(false); const [waiting, setWaiting] = useState(false);
@ -28,15 +28,15 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
const { saveLibraryPanel } = usePanelSave(); const { saveLibraryPanel } = usePanelSave();
const onCreate = useCallback(() => { const onCreate = useCallback(() => {
panel.libraryPanel = { uid: '', name: panelName }; panel.libraryPanel = { uid: '', name: panelName };
saveLibraryPanel(panel, folderId!).then((res) => { saveLibraryPanel(panel, folderUid!).then((res) => {
if (!(res instanceof Error)) { if (!(res instanceof Error)) {
onDismiss(); onDismiss();
} }
}); });
}, [panel, panelName, folderId, onDismiss, saveLibraryPanel]); }, [panel, panelName, folderUid, onDismiss, saveLibraryPanel]);
const isValidName = useAsync(async () => { const isValidName = useAsync(async () => {
try { try {
return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderId === folderId); return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderUid === folderUid);
} catch (err) { } catch (err) {
if (isFetchError(err)) { if (isFetchError(err)) {
err.isHandled = true; err.isHandled = true;
@ -45,7 +45,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
} finally { } finally {
setWaiting(false); setWaiting(false);
} }
}, [debouncedPanelName, folderId]); }, [debouncedPanelName, folderUid]);
const invalidInput = const invalidInput =
!isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting; !isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting;
@ -72,8 +72,8 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
)} )}
> >
<FolderPicker <FolderPicker
onChange={({ id }) => setFolderId(id)} onChange={({ uid }) => setFolderUid(uid)}
initialFolderId={initialFolderId} initialFolderUid={initialFolderUid}
inputId="share-panel-library-panel-folder-picker" inputId="share-panel-library-panel-folder-picker"
/> />
</Field> </Field>
@ -94,10 +94,10 @@ interface Props extends AddLibraryPanelContentsProps {
isOpen?: boolean; isOpen?: boolean;
} }
export const AddLibraryPanelModal = ({ isOpen = false, panel, initialFolderId, ...props }: Props) => { export const AddLibraryPanelModal = ({ isOpen = false, panel, initialFolderUid, ...props }: Props) => {
return ( return (
<Modal title="Create library panel" isOpen={isOpen} onDismiss={props.onDismiss}> <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> </Modal>
); );
}; };

View File

@ -28,9 +28,9 @@ jest.mock('debounce-promise', () => {
const debounce = (fn: any) => { const debounce = (fn: any) => {
const debounced = () => const debounced = () =>
Promise.resolve([ Promise.resolve([
{ label: 'General', value: { id: 0, title: 'General' } }, { label: 'General', value: { uid: '', title: 'General' } },
{ label: 'Folder1', value: { id: 1, title: 'Folder1' } }, { label: 'Folder1', value: { id: 'xMsQdBfWz', title: 'Folder1' } },
{ label: 'Folder2', value: { id: 2, title: 'Folder2' } }, { label: 'Folder2', value: { id: 'wfTJJL5Wz', title: 'Folder2' } },
]); ]);
return debounced; return debounced;
}; };
@ -187,7 +187,7 @@ describe('LibraryPanelsSearch', () => {
kind: LibraryElementKind.Panel, kind: LibraryElementKind.Panel,
uid: 'uid', uid: 'uid',
description: 'Library Panel Description', description: 'Library Panel Description',
folderId: 0, folderUid: '',
model: { type: 'timeseries', title: 'A title' }, model: { type: 'timeseries', title: 'A title' },
type: 'timeseries', type: 'timeseries',
orgId: 1, orgId: 1,
@ -242,7 +242,7 @@ describe('LibraryPanelsSearch', () => {
kind: LibraryElementKind.Panel, kind: LibraryElementKind.Panel,
uid: 'uid', uid: 'uid',
description: 'Library Panel Description', description: 'Library Panel Description',
folderId: 0, folderUid: '',
model: { type: 'timeseries', title: 'A title' }, model: { type: 'timeseries', title: 'A title' },
type: 'timeseries', type: 'timeseries',
orgId: 1, orgId: 1,
@ -286,7 +286,7 @@ describe('LibraryPanelsSearch', () => {
kind: LibraryElementKind.Panel, kind: LibraryElementKind.Panel,
uid: 'uid', uid: 'uid',
description: 'Library Panel Description', description: 'Library Panel Description',
folderId: 0, folderUid: '',
model: { type: 'timeseries', title: 'A title' }, model: { type: 'timeseries', title: 'A title' },
type: 'timeseries', type: 'timeseries',
orgId: 1, orgId: 1,

View File

@ -106,7 +106,7 @@ function mockLibraryPanel({
uid = '1', uid = '1',
id = 1, id = 1,
orgId = 1, orgId = 1,
folderId = 0, folderUid = '',
name = 'Test Panel', name = 'Test Panel',
model = { type: 'text', title: 'Test Panel' }, model = { type: 'text', title: 'Test Panel' },
meta = { meta = {
@ -126,7 +126,7 @@ function mockLibraryPanel({
uid, uid,
id, id,
orgId, orgId,
folderId, folderUid,
name, name,
kind: LibraryElementKind.Panel, kind: LibraryElementKind.Panel,
model, model,

View File

@ -70,7 +70,7 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
<AddLibraryPanelModal <AddLibraryPanelModal
panel={panel} panel={panel}
onDismiss={() => setShowingAddPanelModal(false)} onDismiss={() => setShowingAddPanelModal(false)}
initialFolderId={dashboard?.meta.folderId} initialFolderUid={dashboard?.meta.folderUid}
isOpen={showingAddPanelModal} isOpen={showingAddPanelModal}
/> />
)} )}

View File

@ -10,14 +10,21 @@ import { usePanelSave } from '../../utils/usePanelSave';
interface Props { interface Props {
panel: PanelModelWithLibraryPanel; panel: PanelModelWithLibraryPanel;
folderId: number; folderUid: string;
isUnsavedPrompt?: boolean; isUnsavedPrompt?: boolean;
onConfirm: () => void; onConfirm: () => void;
onDismiss: () => void; onDismiss: () => void;
onDiscard: () => 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 [searchString, setSearchString] = useState('');
const dashState = useAsync(async () => { const dashState = useAsync(async () => {
const searchHits = await getConnectedDashboards(panel.libraryPanel.uid); const searchHits = await getConnectedDashboards(panel.libraryPanel.uid);
@ -98,7 +105,7 @@ export const SaveLibraryPanelModal = ({ panel, folderId, isUnsavedPrompt, onDism
)} )}
<Button <Button
onClick={() => { onClick={() => {
saveLibraryPanel(panel, folderId).then(() => { saveLibraryPanel(panel, folderUid).then(() => {
onConfirm(); onConfirm();
}); });
}} }}

View File

@ -77,10 +77,10 @@ export async function getLibraryPanelByName(name: string): Promise<LibraryElemen
export async function addLibraryPanel( export async function addLibraryPanel(
panelSaveModel: PanelModelWithLibraryPanel, panelSaveModel: PanelModelWithLibraryPanel,
folderId: number folderUid: string
): Promise<LibraryElementDTO> { ): Promise<LibraryElementDTO> {
const { result } = await getBackendSrv().post(`/api/library-elements`, { const { result } = await getBackendSrv().post(`/api/library-elements`, {
folderId, folderUid,
name: panelSaveModel.libraryPanel.name, name: panelSaveModel.libraryPanel.name,
model: panelSaveModel, model: panelSaveModel,
kind: LibraryElementKind.Panel, kind: LibraryElementKind.Panel,

View File

@ -32,7 +32,7 @@ export interface LibraryElementsSearchResult {
export interface LibraryElementDTO { export interface LibraryElementDTO {
id: number; id: number;
orgId: number; orgId: number;
folderId: number; folderUid: string;
uid: string; uid: string;
name: string; name: string;
kind: LibraryElementKind; kind: LibraryElementKind;

View File

@ -13,9 +13,9 @@ export function createPanelLibrarySuccessNotification(message: string): AppNotif
return createSuccessNotification(message); 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 panelSaveModel = toPanelSaveModel(panel);
const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderId); const savedPanel = await saveOrUpdateLibraryPanel(panelSaveModel, folderUid);
updatePanelModelWithUpdate(panel, savedPanel); updatePanelModelWithUpdate(panel, savedPanel);
return savedPanel; return savedPanel;
} }
@ -44,13 +44,13 @@ function updatePanelModelWithUpdate(panel: PanelModel, updated: LibraryElementDT
panel.refresh(); panel.refresh();
} }
function saveOrUpdateLibraryPanel(panel: any, folderId: number): Promise<LibraryElementDTO> { function saveOrUpdateLibraryPanel(panel: any, folderUid: string): Promise<LibraryElementDTO> {
if (!panel.libraryPanel) { if (!panel.libraryPanel) {
return Promise.reject(); return Promise.reject();
} }
if (panel.libraryPanel && panel.libraryPanel.uid === '') { if (panel.libraryPanel && panel.libraryPanel.uid === '') {
return addLibraryPanel(panel, folderId!); return addLibraryPanel(panel, folderUid!);
} }
return updateLibraryPanel(panel); return updateLibraryPanel(panel);

View File

@ -15,9 +15,9 @@ import {
export const usePanelSave = () => { export const usePanelSave = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderId: number) => { const [state, saveLibraryPanel] = useAsyncFn(async (panel: PanelModel, folderUid: string) => {
try { try {
return await saveAndRefreshLibraryPanel(panel, folderId); return await saveAndRefreshLibraryPanel(panel, folderUid);
} catch (err) { } catch (err) {
if (isFetchError(err)) { if (isFetchError(err)) {
err.isHandled = true; err.isHandled = true;

View File

@ -30,7 +30,7 @@ import { ImportDashboardLibraryPanelsList } from './ImportDashboardLibraryPanels
interface Props extends Pick<FormAPI<ImportDashboardDTO>, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> { interface Props extends Pick<FormAPI<ImportDashboardDTO>, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> {
uidReset: boolean; uidReset: boolean;
inputs: DashboardInputs; inputs: DashboardInputs;
initialFolderId: number; initialFolderUid: string;
onCancel: () => void; onCancel: () => void;
onUidReset: () => void; onUidReset: () => void;
@ -44,7 +44,7 @@ export const ImportDashboardForm: FC<Props> = ({
getValues, getValues,
uidReset, uidReset,
inputs, inputs,
initialFolderId, initialFolderUid,
onUidReset, onUidReset,
onCancel, onCancel,
onSubmit, onSubmit,
@ -73,7 +73,7 @@ export const ImportDashboardForm: FC<Props> = ({
<Input <Input
{...register('title', { {...register('title', {
required: 'Name is required', 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" type="text"
data-testid={selectors.components.ImportDashboardForm.name} data-testid={selectors.components.ImportDashboardForm.name}
@ -82,7 +82,7 @@ export const ImportDashboardForm: FC<Props> = ({
<Field label="Folder"> <Field label="Folder">
<InputControl <InputControl
render={({ field: { ref, ...field } }) => ( render={({ field: { ref, ...field } }) => (
<FolderPicker {...field} enableCreateNew initialFolderId={initialFolderId} /> <FolderPicker {...field} enableCreateNew initialFolderUid={initialFolderUid} />
)} )}
name="folder" name="folder"
control={control} control={control}

View File

@ -21,7 +21,7 @@ const mapStateToProps = (state: StoreState) => {
meta: state.importDashboard.meta, meta: state.importDashboard.meta,
source: state.importDashboard.source, source: state.importDashboard.source,
inputs: state.importDashboard.inputs, 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} onUidReset={this.onUidReset}
onSubmit={this.onSubmit} onSubmit={this.onSubmit}
watch={watch} watch={watch}
initialFolderId={folder.id} initialFolderUid={folder.uid}
/> />
)} )}
</Form> </Form>

View File

@ -17,8 +17,8 @@ class ValidationError extends Error {
export class ValidationSrv { export class ValidationSrv {
rootName = 'general'; rootName = 'general';
validateNewDashboardName(folderId: any, name: string) { validateNewDashboardName(folderUid: any, name: string) {
return this.validate(folderId, name, 'A dashboard or a folder with the same name already exists'); return this.validate(folderUid, name, 'A dashboard or a folder with the same name already exists');
} }
validateNewFolderName(name?: string) { validateNewFolderName(name?: string) {

View File

@ -24,7 +24,7 @@ describe('importDashboard', () => {
], ],
elements: [], elements: [],
folder: { folder: {
id: 1, uid: '5v6e5VH4z',
title: 'title', title: 'title',
}, },
}; };
@ -64,7 +64,7 @@ describe('importDashboard', () => {
title: 'Asda', title: 'Asda',
uid: '12', uid: '12',
}, },
folderId: 1, folderUid: '5v6e5VH4z',
inputs: [ inputs: [
{ {
name: 'ds-name', name: 'ds-name',

View File

@ -166,7 +166,7 @@ export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkR
dashboard: { ...dashboard, title: importDashboardForm.title, uid: importDashboardForm.uid || dashboard.uid }, dashboard: { ...dashboard, title: importDashboardForm.title, uid: importDashboardForm.uid || dashboard.uid },
overwrite: true, overwrite: true,
inputs: inputsToPersist, inputs: inputsToPersist,
folderId: importDashboardForm.folder.id, folderUid: importDashboardForm.folder.uid,
}); });
const dashboardUrl = locationUtil.stripBaseFromUrl(result.importedUrl); const dashboardUrl = locationUtil.stripBaseFromUrl(result.importedUrl);
@ -203,13 +203,16 @@ export function moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
async function moveDashboard(uid: string, toFolder: FolderInfo) { async function moveDashboard(uid: string, toFolder: FolderInfo) {
const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${uid}`); 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 }; return { alreadyInFolder: true };
} }
const options = { const options = {
dashboard: fullDash.dashboard, dashboard: fullDash.dashboard,
folderId: toFolder.id, folderUid: toFolder.uid,
overwrite: false, overwrite: false,
}; };
@ -271,7 +274,7 @@ export function saveDashboard(options: SaveDashboardCommand) {
dashboard: options.dashboard, dashboard: options.dashboard,
message: options.message ?? '', message: options.message ?? '',
overwrite: options.overwrite ?? false, 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 }> { export function getFolderById(id: number): Promise<{ id: number; title: string }> {
return getBackendSrv().get(`/api/folders/id/${id}`); return getBackendSrv().get(`/api/folders/id/${id}`);
} }

View File

@ -16,7 +16,7 @@ export interface ImportDashboardDTO {
constants: string[]; constants: string[];
dataSources: DataSourceInstanceSettings[]; dataSources: DataSourceInstanceSettings[];
elements: LibraryElementDTO[]; elements: LibraryElementDTO[];
folder: { id: number; title?: string }; folder: { uid: string; title?: string };
} }
export enum InputType { export enum InputType {

View File

@ -29,9 +29,9 @@ export const validateGcomDashboard = (gcomDashboard: string) => {
return match && (match[1] || match[2]) ? true : 'Could not find a valid Grafana.com ID'; 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 return validationSrv
.validateNewDashboardName(folderId, newTitle) .validateNewDashboardName(folderUid, newTitle)
.then(() => { .then(() => {
return true; return true;
}) })

View File

@ -3,17 +3,17 @@ import React, { FC } from 'react';
import { Menu, Dropdown, Button, Icon } from '@grafana/ui'; import { Menu, Dropdown, Button, Icon } from '@grafana/ui';
export interface Props { export interface Props {
folderId?: number; folderUid?: string;
canCreateFolders?: boolean; canCreateFolders?: boolean;
canCreateDashboards?: 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) => { const actionUrl = (type: string) => {
let url = `dashboard/${type}`; let url = `dashboard/${type}`;
if (folderId) { if (folderUid) {
url += `?folderId=${folderId}`; url += `?folderUid=${folderUid}`;
} }
return url; return url;
@ -23,7 +23,7 @@ export const DashboardActions: FC<Props> = ({ folderId, canCreateFolders = false
return ( return (
<Menu> <Menu>
{canCreateDashboards && <Menu.Item url={actionUrl('new')} label="New Dashboard" />} {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" />} {canCreateDashboards && <Menu.Item url={actionUrl('import')} label="Import" />}
</Menu> </Menu>
); );

View File

@ -24,14 +24,14 @@ export const ManageDashboardsNew = React.memo(({ folder }: Props) => {
const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); const { onKeyDown, keyboardEvents } = useKeyNavigationListener();
// TODO: we need to refactor DashboardActions to use folder.uid instead // 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 canSave = folder?.canSave;
const { isEditor } = contextSrv; const { isEditor } = contextSrv;
const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders; const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders;
const canCreateFolders = contextSrv.hasAccess(AccessControlAction.FoldersCreate, isEditor); const canCreateFolders = contextSrv.hasAccess(AccessControlAction.FoldersCreate, isEditor);
const canCreateDashboardsFallback = hasEditPermissionInFolders || !!canSave; const canCreateDashboardsFallback = hasEditPermissionInFolders || !!canSave;
const canCreateDashboards = folder?.id const canCreateDashboards = folderUid
? contextSrv.hasAccessInMetadata(AccessControlAction.DashboardsCreate, folder, canCreateDashboardsFallback) ? contextSrv.hasAccessInMetadata(AccessControlAction.DashboardsCreate, folder, canCreateDashboardsFallback)
: contextSrv.hasAccess(AccessControlAction.DashboardsCreate, canCreateDashboardsFallback); : contextSrv.hasAccess(AccessControlAction.DashboardsCreate, canCreateDashboardsFallback);
const viewActions = (folder === undefined && canCreateFolders) || canCreateDashboards; const viewActions = (folder === undefined && canCreateFolders) || canCreateDashboards;
@ -55,7 +55,7 @@ export const ManageDashboardsNew = React.memo(({ folder }: Props) => {
</div> </div>
{viewActions && ( {viewActions && (
<DashboardActions <DashboardActions
folderId={folderId} folderUid={folderUid}
canCreateFolders={canCreateFolders} canCreateFolders={canCreateFolders}
canCreateDashboards={canCreateDashboards} canCreateDashboards={canCreateDashboards}
/> />

View File

@ -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 Move the {selectedDashboards.length} selected dashboard{selectedDashboards.length === 1 ? '' : 's'} to the
following folder: following folder:
</p> </p>
<FolderPicker onChange={(f) => setFolder(f as FolderInfo)} /> <FolderPicker onChange={(f) => setFolder(f)} />
</div> </div>
<HorizontalGroup justify="center"> <HorizontalGroup justify="center">

View File

@ -165,7 +165,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
title="This folder doesn't have any dashboards yet" title="This folder doesn't have any dashboards yet"
buttonIcon="plus" buttonIcon="plus"
buttonTitle="Create Dashboard" buttonTitle="Create Dashboard"
buttonLink={`dashboard/new?folderId=${folderDTO.id}`} buttonLink={`dashboard/new?folderUid=${folderDTO.uid}`}
proTip="Add/move dashboards to your folder at ->" proTip="Add/move dashboards to your folder at ->"
proTipLink="dashboards" proTipLink="dashboards"
proTipLinkTitle="Manage dashboards" proTipLinkTitle="Manage dashboards"

View File

@ -256,7 +256,7 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
showRoot={false} showRoot={false}
allowEmpty={true} allowEmpty={true}
initialTitle={props.value?.title} initialTitle={props.value?.title}
initialFolderId={props.value?.id} initialFolderUid={props.value?.uid}
permissionLevel={PermissionLevelString.View} permissionLevel={PermissionLevelString.View}
onClear={() => props.onChange('')} onClear={() => props.onChange('')}
{...props} {...props}

View File

@ -31,7 +31,7 @@ export interface FolderInfo {
/** /**
* @deprecated use uid instead. * @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; uid?: string;
title?: string; title?: string;
url?: string; url?: string;