mirror of
https://github.com/grafana/grafana.git
synced 2024-11-28 03:34:15 -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": [
|
"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"]
|
||||||
|
@ -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();
|
||||||
|
@ -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) => ({
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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!} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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',
|
||||||
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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">
|
||||||
|
@ -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"
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user