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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ const mapStateToProps = (state: StoreState) => {
meta: state.importDashboard.meta,
source: state.importDashboard.source,
inputs: state.importDashboard.inputs,
folder: searchObj.folderId ? { id: Number(searchObj.folderId) } : { id: 0 },
folder: searchObj.folderUid ? { uid: String(searchObj.folderUid) } : { uid: '' },
};
};
@ -111,7 +111,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
onUidReset={this.onUidReset}
onSubmit={this.onSubmit}
watch={watch}
initialFolderId={folder.id}
initialFolderUid={folder.uid}
/>
)}
</Form>

View File

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

View File

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

View File

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

View File

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

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';
};
export const validateTitle = (newTitle: string, folderId: number) => {
export const validateTitle = (newTitle: string, folderUid: string) => {
return validationSrv
.validateNewDashboardName(folderId, newTitle)
.validateNewDashboardName(folderUid, newTitle)
.then(() => {
return true;
})

View File

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

View File

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

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
following folder:
</p>
<FolderPicker onChange={(f) => setFolder(f as FolderInfo)} />
<FolderPicker onChange={(f) => setFolder(f)} />
</div>
<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"
buttonIcon="plus"
buttonTitle="Create Dashboard"
buttonLink={`dashboard/new?folderId=${folderDTO.id}`}
buttonLink={`dashboard/new?folderUid=${folderDTO.uid}`}
proTip="Add/move dashboards to your folder at ->"
proTipLink="dashboards"
proTipLinkTitle="Manage dashboards"

View File

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

View File

@ -31,7 +31,7 @@ export interface FolderInfo {
/**
* @deprecated use uid instead.
*/
id?: number;
id?: number; // can't be totally removed as search and alerts api aren't supporting folderUids yet. It will break DashList and AlertList panel
uid?: string;
title?: string;
url?: string;