Nested folders: very naive implementations of move/delete (#67309)

* implement naive delete

* implement naive move

* use params object instead of appending to url
This commit is contained in:
Ashley Harrison 2023-04-27 15:10:34 +01:00 committed by GitHub
parent 1d99500b3e
commit 4b241311b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 134 additions and 18 deletions

View File

@ -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<BrowseDashboardsPageRo
// New Browse/Manage/Search Dashboards views for nested folders
const BrowseDashboardsPage = memo(({ match }: Props) => {
// 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 ? <BrowseActions /> : <BrowseFilters />}
{hasSelection ? <BrowseActions onActionComplete={() => setRerender(rerender + 1)} /> : <BrowseFilters />}
<div className={styles.subView}>
<AutoSizer>
{({ width, height }) =>
isSearching ? (
<SearchView width={width} height={height} />
<SearchView key={rerender} width={width} height={height} />
) : (
<BrowseView width={width} height={height} folderUID={folderUID} />
<BrowseView key={rerender} width={width} height={height} folderUID={folderUID} />
)
}
</AutoSizer>

View File

@ -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<DeleteDashboardResponse, string>({
query: (dashboardUID) => ({
url: `/dashboards/uid/${dashboardUID}`,
method: 'DELETE',
}),
}),
deleteFolder: builder.mutation<void, string>({
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<FolderDTO, string>({
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';

View File

@ -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 (
<div className={styles.row} data-testid="manage-actions">
<Button onClick={onMove} variant="secondary">
<Button onClick={showMoveModal} variant="secondary">
Move
</Button>
<Button onClick={onDelete} variant="destructive">
<Button onClick={showDeleteModal} variant="destructive">
Delete
</Button>
</div>