mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Nested folders: Consolidate mutations in RTK query (#70249)
* this is an ok intermediate point * delete some unused actions + fix tag invalidation on folder save * remove prefetching for now (it creates a permanent subscription?!) * leave paginated fetch out of rtk query for now * ensure we're invalidating the cache correctly * fix dashboard saving * simplify * recursively invalidate children on rename * tidy up * don't need to invalidate tags on delete * don't need to invalidate on new either * make new refreshParents action * pageheader spacing * invalidate getFolder on move * bit of rearrangement
This commit is contained in:
parent
95b1f3c875
commit
eacb5bee7e
@ -75,7 +75,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
gap: theme.spacing(1, 4),
|
||||
justifyContent: 'space-between',
|
||||
maxWidth: '100%',
|
||||
minWidth: '300px',
|
||||
minWidth: '200px',
|
||||
}),
|
||||
pageHeader: css({
|
||||
label: 'page-header',
|
||||
|
@ -1,17 +1,33 @@
|
||||
import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { isTruthy } from '@grafana/data';
|
||||
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
|
||||
import { DescendantCount, DescendantCountDTO, FolderDTO } from 'app/types';
|
||||
import { isTruthy, locationUtil } from '@grafana/data';
|
||||
import { BackendSrvRequest, getBackendSrv, locationService } from '@grafana/runtime';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { DashboardDTO, DescendantCount, DescendantCountDTO, FolderDTO, SaveDashboardResponseDTO } from 'app/types';
|
||||
|
||||
import { refetchChildren, refreshParents } from '../state';
|
||||
import { DashboardTreeSelection } from '../types';
|
||||
|
||||
import { PAGE_SIZE, ROOT_PAGE_SIZE } from './services';
|
||||
|
||||
interface RequestOptions extends BackendSrvRequest {
|
||||
manageError?: (err: unknown) => { error: unknown };
|
||||
showErrorAlert?: boolean;
|
||||
}
|
||||
|
||||
interface DeleteItemsArgs {
|
||||
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>;
|
||||
}
|
||||
|
||||
interface MoveItemsArgs extends DeleteItemsArgs {
|
||||
destinationUID: string;
|
||||
}
|
||||
|
||||
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
|
||||
async function backendSrvBaseQuery(requestOptions: RequestOptions) {
|
||||
try {
|
||||
@ -36,22 +52,109 @@ export const browseDashboardsAPI = createApi({
|
||||
reducerPath: 'browseDashboardsAPI',
|
||||
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
|
||||
endpoints: (builder) => ({
|
||||
// get folder info (e.g. title, parents) but *not* children
|
||||
getFolder: builder.query<FolderDTO, string>({
|
||||
providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }],
|
||||
query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }),
|
||||
providesTags: (_result, _error, arg) => [{ type: 'getFolder', id: arg }],
|
||||
}),
|
||||
saveFolder: builder.mutation<FolderDTO, FolderDTO>({
|
||||
invalidatesTags: (_result, _error, args) => [{ type: 'getFolder', id: args.uid }],
|
||||
query: (folder) => ({
|
||||
method: 'PUT',
|
||||
showErrorAlert: false,
|
||||
url: `/folders/${folder.uid}`,
|
||||
// create a new folder
|
||||
newFolder: builder.mutation<FolderDTO, { title: string; parentUid?: string }>({
|
||||
query: ({ title, parentUid }) => ({
|
||||
method: 'POST',
|
||||
url: '/folders',
|
||||
data: {
|
||||
title: folder.title,
|
||||
version: folder.version,
|
||||
title,
|
||||
parentUid,
|
||||
},
|
||||
}),
|
||||
onQueryStarted: ({ parentUid }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(async ({ data: folder }) => {
|
||||
await contextSrv.fetchUserPermissions();
|
||||
dispatch(notifyApp(createSuccessNotification('Folder created')));
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: parentUid,
|
||||
pageSize: parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
locationService.push(locationUtil.stripBaseFromUrl(folder.url));
|
||||
});
|
||||
},
|
||||
}),
|
||||
// save an existing folder (e.g. rename)
|
||||
saveFolder: builder.mutation<FolderDTO, FolderDTO>({
|
||||
// because the getFolder calls contain the parents, renaming a parent/grandparent/etc needs to invalidate all child folders
|
||||
// we could do something smart and recursively invalidate these child folders but it doesn't seem worth it
|
||||
// instead let's just invalidate all the getFolder calls
|
||||
invalidatesTags: ['getFolder'],
|
||||
query: ({ uid, title, version }) => ({
|
||||
method: 'PUT',
|
||||
url: `/folders/${uid}`,
|
||||
data: {
|
||||
title,
|
||||
version,
|
||||
},
|
||||
}),
|
||||
onQueryStarted: ({ parentUid }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: parentUid,
|
||||
pageSize: parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
}),
|
||||
// move an *individual* folder. used in the folder actions menu.
|
||||
moveFolder: builder.mutation<void, { folder: FolderDTO; destinationUID: string }>({
|
||||
invalidatesTags: ['getFolder'],
|
||||
query: ({ folder, destinationUID }) => ({
|
||||
url: `/folders/${folder.uid}/move`,
|
||||
method: 'POST',
|
||||
data: { parentUID: destinationUID },
|
||||
}),
|
||||
onQueryStarted: ({ folder, destinationUID }, { queryFulfilled, dispatch }) => {
|
||||
const { parentUid } = folder;
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: parentUid,
|
||||
pageSize: parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: destinationUID,
|
||||
pageSize: destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
}),
|
||||
// delete an *individual* folder. used in the folder actions menu.
|
||||
deleteFolder: builder.mutation<void, FolderDTO>({
|
||||
query: ({ uid }) => ({
|
||||
url: `/folders/${uid}`,
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
// TODO: Once backend returns alert rule counts, set this back to true
|
||||
// when this is merged https://github.com/grafana/grafana/pull/67259
|
||||
forceDeleteRules: false,
|
||||
},
|
||||
}),
|
||||
onQueryStarted: ({ parentUid }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: parentUid,
|
||||
pageSize: parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
}),
|
||||
// gets the descendant counts for a folder. used in the move/delete modals.
|
||||
getAffectedItems: builder.query<DescendantCount, DashboardTreeSelection>({
|
||||
queryFn: async (selectedItems) => {
|
||||
const folderUIDs = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
@ -81,17 +184,132 @@ export const browseDashboardsAPI = createApi({
|
||||
return { data: totalCounts };
|
||||
},
|
||||
}),
|
||||
moveFolder: builder.mutation<void, { folderUID: string; destinationUID: string }>({
|
||||
query: ({ folderUID, destinationUID }) => ({
|
||||
url: `/folders/${folderUID}/move`,
|
||||
// move *multiple* items (folders and dashboards). used in the move modal.
|
||||
moveItems: builder.mutation<void, MoveItemsArgs>({
|
||||
invalidatesTags: ['getFolder'],
|
||||
queryFn: async ({ selectedItems, destinationUID }, _api, _extraOptions, baseQuery) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
|
||||
// Move all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of selectedFolders) {
|
||||
await baseQuery({
|
||||
url: `/folders/${folderUID}/move`,
|
||||
method: 'POST',
|
||||
data: { parentUID: destinationUID },
|
||||
});
|
||||
}
|
||||
|
||||
// Move all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${dashboardUID}`);
|
||||
|
||||
const options = {
|
||||
dashboard: fullDash.dashboard,
|
||||
folderUid: destinationUID,
|
||||
overwrite: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
await baseQuery({
|
||||
url: `/dashboards/db`,
|
||||
method: 'POST',
|
||||
data: options,
|
||||
});
|
||||
}
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ destinationUID, selectedItems }, { queryFulfilled, dispatch }) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: destinationUID,
|
||||
pageSize: destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
dispatch(refreshParents([...selectedFolders, ...selectedDashboards]));
|
||||
});
|
||||
},
|
||||
}),
|
||||
// delete *multiple* items (folders and dashboards). used in the delete modal.
|
||||
deleteItems: builder.mutation<void, DeleteItemsArgs>({
|
||||
queryFn: async ({ selectedItems }, _api, _extraOptions, baseQuery) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
// Delete all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of selectedFolders) {
|
||||
await baseQuery({
|
||||
url: `/folders/${folderUID}`,
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
// TODO: Once backend returns alert rule counts, set this back to true
|
||||
// when this is merged https://github.com/grafana/grafana/pull/67259
|
||||
forceDeleteRules: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Delete all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
await baseQuery({
|
||||
url: `/dashboards/uid/${dashboardUID}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ selectedItems }, { queryFulfilled, dispatch }) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(refreshParents([...selectedFolders, ...selectedDashboards]));
|
||||
});
|
||||
},
|
||||
}),
|
||||
// save an existing dashboard
|
||||
saveDashboard: builder.mutation<SaveDashboardResponseDTO, SaveDashboardCommand>({
|
||||
query: ({ dashboard, folderUid, message, overwrite }) => ({
|
||||
url: `/dashboards/db`,
|
||||
method: 'POST',
|
||||
data: { parentUID: destinationUID },
|
||||
data: {
|
||||
dashboard,
|
||||
folderUid,
|
||||
message: message ?? '',
|
||||
overwrite: Boolean(overwrite),
|
||||
},
|
||||
}),
|
||||
invalidatesTags: (_result, _error, arg) => [{ type: 'getFolder', id: arg.folderUID }],
|
||||
onQueryStarted: ({ folderUid }, { queryFulfilled, dispatch }) => {
|
||||
dashboardWatcher.ignoreNextSave();
|
||||
queryFulfilled.then(async () => {
|
||||
await contextSrv.fetchUserPermissions();
|
||||
dispatch(
|
||||
refetchChildren({
|
||||
parentUID: folderUid,
|
||||
pageSize: folderUid ? PAGE_SIZE : ROOT_PAGE_SIZE,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { endpoints, useGetAffectedItemsQuery, useGetFolderQuery, useMoveFolderMutation, useSaveFolderMutation } =
|
||||
browseDashboardsAPI;
|
||||
export const {
|
||||
endpoints,
|
||||
useDeleteFolderMutation,
|
||||
useDeleteItemsMutation,
|
||||
useGetAffectedItemsQuery,
|
||||
useGetFolderQuery,
|
||||
useMoveFolderMutation,
|
||||
useMoveItemsMutation,
|
||||
useNewFolderMutation,
|
||||
useSaveDashboardMutation,
|
||||
useSaveFolderMutation,
|
||||
} = browseDashboardsAPI;
|
||||
export { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
|
@ -7,22 +7,12 @@ import { Button, useStyles2 } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
|
||||
import { useDispatch, useSelector } from 'app/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { useMoveFolderMutation } from '../../api/browseDashboardsAPI';
|
||||
import { PAGE_SIZE, ROOT_PAGE_SIZE } from '../../api/services';
|
||||
import {
|
||||
childrenByParentUIDSelector,
|
||||
deleteDashboard,
|
||||
deleteFolder,
|
||||
moveDashboard,
|
||||
refetchChildren,
|
||||
rootItemsSelector,
|
||||
setAllSelection,
|
||||
useActionSelectionState,
|
||||
} from '../../state';
|
||||
import { findItem } from '../../state/utils';
|
||||
import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI';
|
||||
import { setAllSelection, useActionSelectionState } from '../../state';
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
|
||||
import { DeleteModal } from './DeleteModal';
|
||||
import { MoveModal } from './MoveModal';
|
||||
@ -31,77 +21,33 @@ export interface Props {}
|
||||
|
||||
export function BrowseActions() {
|
||||
const styles = useStyles2(getStyles);
|
||||
const selectedItems = useActionSelectionState();
|
||||
const dispatch = useDispatch();
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
const rootItems = useSelector(rootItemsSelector);
|
||||
const [moveFolder] = useMoveFolderMutation();
|
||||
const childrenByParentUID = useSelector(childrenByParentUIDSelector);
|
||||
const selectedItems = useActionSelectionState();
|
||||
const [deleteItems] = useDeleteItemsMutation();
|
||||
const [moveItems] = useMoveItemsMutation();
|
||||
const [, stateManager] = useSearchStateManager();
|
||||
|
||||
const isSearching = stateManager.hasSearchFilters();
|
||||
|
||||
const onActionComplete = (parentsToRefresh: Set<string | undefined>) => {
|
||||
const onActionComplete = () => {
|
||||
dispatch(setAllSelection({ isSelected: false, folderUID: undefined }));
|
||||
|
||||
if (isSearching) {
|
||||
// Redo search query
|
||||
stateManager.doSearchWithDebounce();
|
||||
} else {
|
||||
// Refetch parents
|
||||
for (const parentUID of parentsToRefresh) {
|
||||
dispatch(refetchChildren({ parentUID, pageSize: parentUID ? PAGE_SIZE : ROOT_PAGE_SIZE }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
const parentsToRefresh = new Set<string | undefined>();
|
||||
|
||||
// Delete all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of selectedFolders) {
|
||||
await dispatch(deleteFolder(folderUID));
|
||||
// find the parent folder uid and add it to parentsToRefresh
|
||||
const folder = findItem(rootItems?.items ?? [], childrenByParentUID, folderUID);
|
||||
parentsToRefresh.add(folder?.parentUID);
|
||||
}
|
||||
|
||||
// Delete all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
await dispatch(deleteDashboard(dashboardUID));
|
||||
// find the parent folder uid and add it to parentsToRefresh
|
||||
const dashboard = findItem(rootItems?.items ?? [], childrenByParentUID, dashboardUID);
|
||||
parentsToRefresh.add(dashboard?.parentUID);
|
||||
}
|
||||
trackAction('delete', selectedDashboards, selectedFolders);
|
||||
onActionComplete(parentsToRefresh);
|
||||
await deleteItems({ selectedItems });
|
||||
trackAction('delete', selectedItems);
|
||||
onActionComplete();
|
||||
};
|
||||
|
||||
const onMove = async (destinationUID: string) => {
|
||||
const parentsToRefresh = new Set<string | undefined>();
|
||||
parentsToRefresh.add(destinationUID);
|
||||
|
||||
// Move all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of selectedFolders) {
|
||||
await moveFolder({ folderUID, destinationUID });
|
||||
// find the parent folder uid and add it to parentsToRefresh
|
||||
const folder = findItem(rootItems?.items ?? [], childrenByParentUID, folderUID);
|
||||
parentsToRefresh.add(folder?.parentUID);
|
||||
}
|
||||
|
||||
// Move all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
await dispatch(moveDashboard({ dashboardUID, destinationUID }));
|
||||
// find the parent folder uid and add it to parentsToRefresh
|
||||
const dashboard = findItem(rootItems?.items ?? [], childrenByParentUID, dashboardUID);
|
||||
parentsToRefresh.add(dashboard?.parentUID);
|
||||
}
|
||||
trackAction('move', selectedDashboards, selectedFolders);
|
||||
onActionComplete(parentsToRefresh);
|
||||
await moveItems({ selectedItems, destinationUID });
|
||||
trackAction('move', selectedItems);
|
||||
onActionComplete();
|
||||
};
|
||||
|
||||
const showMoveModal = () => {
|
||||
@ -156,7 +102,10 @@ const actionMap: Record<actionType, string> = {
|
||||
delete: 'grafana_manage_dashboards_item_deleted',
|
||||
};
|
||||
|
||||
function trackAction(action: actionType, selectedDashboards: string[], selectedFolders: string[]) {
|
||||
function trackAction(action: actionType, selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>) {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
|
||||
reportInteraction(actionMap[action], {
|
||||
item_counts: {
|
||||
folder: selectedFolders.length,
|
||||
|
@ -1,19 +1,20 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { PAGE_SIZE, ROOT_PAGE_SIZE } from '../api/services';
|
||||
import { PAGE_SIZE } from '../api/services';
|
||||
import {
|
||||
useFlatTreeState,
|
||||
useCheckboxSelectionState,
|
||||
fetchNextChildrenPage,
|
||||
setFolderOpenState,
|
||||
setItemSelectionState,
|
||||
useChildrenByParentUIDState,
|
||||
setAllSelection,
|
||||
useBrowseLoadingStatus,
|
||||
useLoadNextChildrenPage,
|
||||
fetchNextChildrenPage,
|
||||
} from '../state';
|
||||
import { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types';
|
||||
|
||||
@ -163,23 +164,3 @@ function hasSelectedDescendants(
|
||||
return hasSelectedDescendants(v, childrenByParentUID, selectedItems);
|
||||
});
|
||||
}
|
||||
|
||||
function useLoadNextChildrenPage(folderUID: string | undefined) {
|
||||
const dispatch = useDispatch();
|
||||
const requestInFlightRef = useRef(false);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (requestInFlightRef.current) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
requestInFlightRef.current = true;
|
||||
|
||||
const promise = dispatch(fetchNextChildrenPage({ parentUID: folderUID, pageSize: ROOT_PAGE_SIZE }));
|
||||
promise.finally(() => (requestInFlightRef.current = false));
|
||||
|
||||
return promise;
|
||||
}, [dispatch, folderUID]);
|
||||
|
||||
return handleLoadMore;
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
|
||||
import { createNewFolder } from 'app/features/folders/state/actions';
|
||||
import {
|
||||
getNewDashboardPhrase,
|
||||
getNewFolderPhrase,
|
||||
@ -13,35 +11,36 @@ import {
|
||||
} from 'app/features/search/tempI18nPhrases';
|
||||
import { FolderDTO } from 'app/types';
|
||||
|
||||
import { useNewFolderMutation } from '../api/browseDashboardsAPI';
|
||||
|
||||
import { NewFolderForm } from './NewFolderForm';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
createNewFolder,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {
|
||||
interface Props {
|
||||
parentFolder?: FolderDTO;
|
||||
canCreateFolder: boolean;
|
||||
canCreateDashboard: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder, createNewFolder }: Props) {
|
||||
export default function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const [newFolder] = useNewFolderMutation();
|
||||
const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false);
|
||||
|
||||
const onCreateFolder = (folderName: string) => {
|
||||
createNewFolder(folderName, parentFolder?.uid);
|
||||
const depth = parentFolder?.parents ? parentFolder.parents.length + 1 : 0;
|
||||
reportInteraction('grafana_manage_dashboards_folder_created', {
|
||||
is_subfolder: Boolean(parentFolder?.uid),
|
||||
folder_depth: depth,
|
||||
});
|
||||
setShowNewFolderDrawer(false);
|
||||
const onCreateFolder = async (folderName: string) => {
|
||||
try {
|
||||
await newFolder({
|
||||
title: folderName,
|
||||
parentUid: parentFolder?.uid,
|
||||
});
|
||||
const depth = parentFolder?.parents ? parentFolder.parents.length + 1 : 0;
|
||||
reportInteraction('grafana_manage_dashboards_folder_created', {
|
||||
is_subfolder: Boolean(parentFolder?.uid),
|
||||
folder_depth: depth,
|
||||
});
|
||||
} finally {
|
||||
setShowNewFolderDrawer(false);
|
||||
}
|
||||
};
|
||||
|
||||
const newMenu = (
|
||||
@ -97,8 +96,6 @@ function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder, cr
|
||||
);
|
||||
}
|
||||
|
||||
export default connector(CreateNewButton);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url without any parameters
|
||||
|
@ -4,12 +4,10 @@ import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
|
||||
import { Permissions } from 'app/core/components/AccessControl';
|
||||
import { appEvents, contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, FolderDTO, useDispatch } from 'app/types';
|
||||
import { AccessControlAction, FolderDTO } from 'app/types';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { useMoveFolderMutation } from '../api/browseDashboardsAPI';
|
||||
import { PAGE_SIZE, ROOT_PAGE_SIZE } from '../api/services';
|
||||
import { deleteFolder, refetchChildren } from '../state';
|
||||
import { useDeleteFolderMutation, useMoveFolderMutation } from '../api/browseDashboardsAPI';
|
||||
|
||||
import { DeleteModal } from './BrowseActions/DeleteModal';
|
||||
import { MoveModal } from './BrowseActions/MoveModal';
|
||||
@ -19,17 +17,17 @@ interface Props {
|
||||
}
|
||||
|
||||
export function FolderActionsButton({ folder }: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||
const [moveFolder] = useMoveFolderMutation();
|
||||
const [deleteFolder] = useDeleteFolderMutation();
|
||||
const canViewPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsRead);
|
||||
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsWrite);
|
||||
const canMoveFolder = contextSrv.hasPermission(AccessControlAction.FoldersWrite);
|
||||
const canDeleteFolder = contextSrv.hasPermission(AccessControlAction.FoldersDelete);
|
||||
|
||||
const onMove = async (destinationUID: string) => {
|
||||
await moveFolder({ folderUID: folder.uid, destinationUID });
|
||||
await moveFolder({ folder, destinationUID });
|
||||
reportInteraction('grafana_manage_dashboards_item_moved', {
|
||||
item_counts: {
|
||||
folder: 1,
|
||||
@ -37,17 +35,10 @@ export function FolderActionsButton({ folder }: Props) {
|
||||
},
|
||||
source: 'folder_actions',
|
||||
});
|
||||
dispatch(refetchChildren({ parentUID: destinationUID, pageSize: destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE }));
|
||||
|
||||
if (folder.parentUid) {
|
||||
dispatch(
|
||||
refetchChildren({ parentUID: folder.parentUid, pageSize: folder.parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await dispatch(deleteFolder(folder.uid));
|
||||
await deleteFolder(folder);
|
||||
reportInteraction('grafana_manage_dashboards_item_deleted', {
|
||||
item_counts: {
|
||||
folder: 1,
|
||||
@ -55,12 +46,9 @@ export function FolderActionsButton({ folder }: Props) {
|
||||
},
|
||||
source: 'folder_actions',
|
||||
});
|
||||
if (folder.parentUid) {
|
||||
dispatch(
|
||||
refetchChildren({ parentUID: folder.parentUid, pageSize: folder.parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE })
|
||||
);
|
||||
}
|
||||
locationService.push('/dashboards');
|
||||
const { parents } = folder;
|
||||
const parentUrl = parents && parents.length ? parents[parents.length - 1].url : '/dashboards';
|
||||
locationService.push(parentUrl);
|
||||
};
|
||||
|
||||
const showMoveModal = () => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types';
|
||||
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
|
||||
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
|
||||
import { createAsyncThunk, DashboardDTO } from 'app/types';
|
||||
import { createAsyncThunk } from 'app/types';
|
||||
|
||||
import { listDashboards, listFolders } from '../api/services';
|
||||
import { listDashboards, listFolders, PAGE_SIZE, ROOT_PAGE_SIZE } from '../api/services';
|
||||
|
||||
import { findItem } from './utils';
|
||||
|
||||
interface FetchNextChildrenPageArgs {
|
||||
parentUID: string | undefined;
|
||||
@ -30,6 +30,25 @@ interface RefetchChildrenResult {
|
||||
lastPageOfKind: boolean;
|
||||
}
|
||||
|
||||
export const refreshParents = createAsyncThunk(
|
||||
'browseDashboards/refreshParents',
|
||||
async (uids: string[], { getState, dispatch }) => {
|
||||
const { browseDashboards } = getState();
|
||||
const { rootItems, childrenByParentUID } = browseDashboards;
|
||||
const parentsToRefresh = new Set<string | undefined>();
|
||||
|
||||
for (const uid of uids) {
|
||||
// find the parent folder uid
|
||||
const item = findItem(rootItems?.items ?? [], childrenByParentUID, uid);
|
||||
parentsToRefresh.add(item?.parentUID);
|
||||
}
|
||||
|
||||
for (const parentUID of parentsToRefresh) {
|
||||
dispatch(refetchChildren({ parentUID, pageSize: parentUID ? PAGE_SIZE : ROOT_PAGE_SIZE }));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const refetchChildren = createAsyncThunk(
|
||||
'browseDashboards/refetchChildren',
|
||||
async ({ parentUID, pageSize }: RefetchChildrenArgs): Promise<RefetchChildrenResult> => {
|
||||
@ -125,39 +144,9 @@ export const fetchNextChildrenPage = createAsyncThunk(
|
||||
|
||||
return {
|
||||
children,
|
||||
lastPageOfKind: lastPageOfKind,
|
||||
lastPageOfKind,
|
||||
page,
|
||||
kind: fetchKind,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteDashboard = createAsyncThunk('browseDashboards/deleteDashboard', async (dashboardUID: string) => {
|
||||
return getBackendSrv().delete<DeleteDashboardResponse>(`/api/dashboards/uid/${dashboardUID}`);
|
||||
});
|
||||
|
||||
export const deleteFolder = createAsyncThunk('browseDashboards/deleteFolder', async (folderUID: string) => {
|
||||
return getBackendSrv().delete(`/api/folders/${folderUID}`, undefined, {
|
||||
// TODO: Revisit this field when this permissions issue is resolved
|
||||
// https://github.com/grafana/grafana-enterprise/issues/5144
|
||||
params: { forceDeleteRules: false },
|
||||
});
|
||||
});
|
||||
|
||||
export const moveDashboard = createAsyncThunk(
|
||||
'browseDashboards/moveDashboard',
|
||||
async ({ dashboardUID, destinationUID }: { dashboardUID: string; destinationUID: string }) => {
|
||||
const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${dashboardUID}`);
|
||||
|
||||
const options = {
|
||||
dashboard: fullDash.dashboard,
|
||||
folderUid: destinationUID,
|
||||
overwrite: false,
|
||||
};
|
||||
|
||||
return getBackendSrv().post('/api/dashboards/db', {
|
||||
message: '',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useSelector, StoreState } from 'app/types';
|
||||
import { useSelector, StoreState, useDispatch } from 'app/types';
|
||||
|
||||
import { ROOT_PAGE_SIZE } from '../api/services';
|
||||
import { BrowseDashboardsState, DashboardsTreeItem, DashboardTreeSelection } from '../types';
|
||||
|
||||
import { fetchNextChildrenPage } from './actions';
|
||||
|
||||
export const rootItemsSelector = (wholeState: StoreState) => wholeState.browseDashboards.rootItems;
|
||||
export const childrenByParentUIDSelector = (wholeState: StoreState) => wholeState.browseDashboards.childrenByParentUID;
|
||||
export const openFoldersSelector = (wholeState: StoreState) => wholeState.browseDashboards.openFolders;
|
||||
@ -94,6 +97,26 @@ export function useActionSelectionState() {
|
||||
return useSelector((state) => selectedItemsForActionsSelector(state));
|
||||
}
|
||||
|
||||
export function useLoadNextChildrenPage(folderUID: string | undefined) {
|
||||
const dispatch = useDispatch();
|
||||
const requestInFlightRef = useRef(false);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (requestInFlightRef.current) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
requestInFlightRef.current = true;
|
||||
|
||||
const promise = dispatch(fetchNextChildrenPage({ parentUID: folderUID, pageSize: ROOT_PAGE_SIZE }));
|
||||
promise.finally(() => (requestInFlightRef.current = false));
|
||||
|
||||
return promise;
|
||||
}, [dispatch, folderUID]);
|
||||
|
||||
return handleLoadMore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of items, with level indicating it's 'nested' in the tree structure
|
||||
*
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { updateDashboardName } from 'app/core/reducers/navBarTree';
|
||||
import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashboards/state/actions';
|
||||
import { useDispatch } from 'app/types';
|
||||
@ -28,10 +29,19 @@ const saveDashboard = async (saveModel: any, options: SaveDashboardOptions, dash
|
||||
export const useDashboardSave = (dashboard: DashboardModel, isCopy = false) => {
|
||||
const dispatch = useDispatch();
|
||||
const notifyApp = useAppNotification();
|
||||
const [saveDashboardRtkQuery] = useSaveDashboardMutation();
|
||||
const [state, onDashboardSave] = useAsyncFn(
|
||||
async (clone: DashboardModel, options: SaveDashboardOptions, dashboard: DashboardModel) => {
|
||||
try {
|
||||
const result = await saveDashboard(clone, options, dashboard);
|
||||
const queryResult = config.featureToggles.nestedFolders
|
||||
? await saveDashboardRtkQuery({
|
||||
dashboard: clone,
|
||||
folderUid: options.folderUid ?? dashboard.meta.folderUid ?? clone.meta.folderUid,
|
||||
message: options.message,
|
||||
overwrite: options.overwrite,
|
||||
})
|
||||
: await saveDashboard(clone, options, dashboard);
|
||||
const result = config.featureToggles.nestedFolders ? queryResult.data : queryResult;
|
||||
dashboard.version = result.version;
|
||||
dashboard.clearUnsavedChanges();
|
||||
|
||||
|
@ -10,6 +10,15 @@ export interface DashboardDTO {
|
||||
meta: DashboardMeta;
|
||||
}
|
||||
|
||||
export interface SaveDashboardResponseDTO {
|
||||
id: number;
|
||||
slug: string;
|
||||
status: string;
|
||||
uid: string;
|
||||
url: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface DashboardMeta {
|
||||
slug?: string;
|
||||
uid?: string;
|
||||
|
Loading…
Reference in New Issue
Block a user