From 4b241311b372b1e0a5b45b7217eca391d7223a3a Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 27 Apr 2023 15:10:34 +0100 Subject: [PATCH] Nested folders: very naive implementations of move/delete (#67309) * implement naive delete * implement naive move * use params object instead of appending to url --- .../BrowseDashboardsPage.tsx | 11 +-- .../api/browseDashboardsAPI.ts | 69 +++++++++++++++++- .../BrowseActions/BrowseActions.tsx | 72 +++++++++++++++---- 3 files changed, 134 insertions(+), 18 deletions(-) diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 72fdbe3903b..63421067dd9 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { memo, useEffect, useMemo } from 'react'; +import React, { memo, useEffect, useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; @@ -29,6 +29,9 @@ export interface Props extends GrafanaRouteComponentProps { + // this is a complete hack to force a full rerender. + // TODO remove once we move everything to RTK query + const [rerender, setRerender] = useState(0); const { uid: folderUID } = match.params; const styles = useStyles2(getStyles); @@ -59,15 +62,15 @@ const BrowseDashboardsPage = memo(({ match }: Props) => { onChange={(e) => stateManager.onQueryChange(e)} /> - {hasSelection ? : } + {hasSelection ? setRerender(rerender + 1)} /> : }
{({ width, height }) => isSearching ? ( - + ) : ( - + ) } diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 3802bdf9326..b5c879c0abb 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -3,7 +3,8 @@ import { lastValueFrom } from 'rxjs'; import { isTruthy } from '@grafana/data'; import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; -import { FolderDTO } from 'app/types'; +import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; +import { DashboardDTO, FolderDTO } from 'app/types'; import { DashboardTreeSelection } from '../types'; @@ -35,6 +36,62 @@ export const browseDashboardsAPI = createApi({ reducerPath: 'browseDashboardsAPI', baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ + deleteDashboard: builder.mutation({ + query: (dashboardUID) => ({ + url: `/dashboards/uid/${dashboardUID}`, + method: 'DELETE', + }), + }), + deleteFolder: builder.mutation({ + query: (folderUID) => ({ + url: `/folders/${folderUID}`, + method: 'DELETE', + params: { + forceDeleteRules: true, + }, + }), + }), + // TODO we can define this return type properly + moveDashboard: builder.mutation< + unknown, + { + dashboardUID: string; + destinationUID: string; + } + >({ + queryFn: async ({ dashboardUID, destinationUID }, _api, _extraOptions, baseQuery) => { + const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${dashboardUID}`); + + const options = { + dashboard: fullDash.dashboard, + folderUid: destinationUID, + overwrite: false, + }; + + return baseQuery({ + url: '/dashboards/db', + method: 'POST', + data: { + message: '', + ...options, + }, + }); + }, + }), + // TODO this doesn't return void, find where the correct type is + moveFolder: builder.mutation< + void, + { + folderUID: string; + destinationUID: string; + } + >({ + query: ({ folderUID, destinationUID }) => ({ + url: `/folders/${folderUID}/move`, + method: 'POST', + data: { parentUid: destinationUID }, + }), + }), getFolder: builder.query({ query: (folderUID) => ({ url: `/folders/${folderUID}` }), }), @@ -93,5 +150,13 @@ export const browseDashboardsAPI = createApi({ }), }); -export const { useGetFolderQuery, useLazyGetFolderQuery, useGetAffectedItemsQuery } = browseDashboardsAPI; +export const { + useDeleteDashboardMutation, + useDeleteFolderMutation, + useGetAffectedItemsQuery, + useGetFolderQuery, + useLazyGetFolderQuery, + useMoveDashboardMutation, + useMoveFolderMutation, +} = browseDashboardsAPI; export { skipToken } from '@reduxjs/toolkit/query/react'; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index 235cddea5b4..4b40aad2740 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -6,40 +6,88 @@ import { Button, useStyles2 } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; +import { + useDeleteDashboardMutation, + useDeleteFolderMutation, + useMoveDashboardMutation, + useMoveFolderMutation, +} from '../../api/browseDashboardsAPI'; import { useActionSelectionState } from '../../state'; import { DeleteModal } from './DeleteModal'; import { MoveModal } from './MoveModal'; -export interface Props {} +export interface Props { + // this is a complete hack to force a full rerender. + // TODO remove once we move everything to RTK query + onActionComplete?: () => void; +} -export function BrowseActions() { +export function BrowseActions({ onActionComplete }: Props) { const styles = useStyles2(getStyles); const selectedItems = useActionSelectionState(); + const [deleteDashboard] = useDeleteDashboardMutation(); + const [deleteFolder] = useDeleteFolderMutation(); + const [moveFolder] = useMoveFolderMutation(); + const [moveDashboard] = useMoveDashboardMutation(); + const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]); + const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); - const onMove = () => { + const onDelete = async () => { + // Delete all the folders sequentially + // TODO error handling here + for (const folderUID of selectedFolders) { + await deleteFolder(folderUID).unwrap(); + } + + // Delete all the dashboards sequenetially + // TODO error handling here + for (const dashboardUID of selectedDashboards) { + await deleteDashboard(dashboardUID).unwrap(); + } + onActionComplete?.(); + }; + + const onMove = async (destinationUID: string) => { + // Move all the folders sequentially + // TODO error handling here + for (const folderUID of selectedFolders) { + await moveFolder({ + folderUID, + destinationUID, + }).unwrap(); + } + + // Move all the dashboards sequentially + // TODO error handling here + for (const dashboardUID of selectedDashboards) { + await moveDashboard({ + dashboardUID, + destinationUID, + }).unwrap(); + } + onActionComplete?.(); + }; + + const showMoveModal = () => { appEvents.publish( new ShowModalReactEvent({ component: MoveModal, props: { selectedItems, - onConfirm: (moveTarget: string) => { - console.log(`MoveModal onConfirm clicked with target ${moveTarget}!`); - }, + onConfirm: onMove, }, }) ); }; - const onDelete = () => { + const showDeleteModal = () => { appEvents.publish( new ShowModalReactEvent({ component: DeleteModal, props: { selectedItems, - onConfirm: () => { - console.log('DeleteModal onConfirm clicked!'); - }, + onConfirm: onDelete, }, }) ); @@ -47,10 +95,10 @@ export function BrowseActions() { return (
- -