Search (Playground) Implement Delete and Move actions in New Search (#48863)

This commit is contained in:
Maria Alexandra 2022-05-11 14:02:05 +02:00 committed by GitHub
parent 270e38cfcf
commit d5a881598f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 286 additions and 6 deletions

View File

@ -5,5 +5,6 @@ export const SEARCH_ITEM_MARGIN = 8;
export const DEFAULT_SORT = { label: 'A\u2013Z', value: 'alpha-asc' }; export const DEFAULT_SORT = { label: 'A\u2013Z', value: 'alpha-asc' };
export const SECTION_STORAGE_KEY = 'search.sections'; export const SECTION_STORAGE_KEY = 'search.sections';
export const GENERAL_FOLDER_ID = 0; export const GENERAL_FOLDER_ID = 0;
export const GENERAL_FOLDER_UID = 'GeneralFolderUID';
export const GENERAL_FOLDER_TITLE = 'General'; export const GENERAL_FOLDER_TITLE = 'General';
export const PREVIEWS_LOCAL_STORAGE_KEY = 'grafana.dashboard.previews'; export const PREVIEWS_LOCAL_STORAGE_KEY = 'grafana.dashboard.previews';

View File

@ -0,0 +1,30 @@
import { render, screen, within } from '@testing-library/react';
import React from 'react';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
describe('ConfirmModal', () => {
it('should render correct title, body, dismiss-, cancel- and delete-text', () => {
const items = new Map();
const dashboardsUIDs = new Set();
dashboardsUIDs.add('uid1');
dashboardsUIDs.add('uid2');
items.set('dashboard', dashboardsUIDs);
const isDeleteModalOpen = true;
const onDeleteItems = jest.fn();
render(
<ConfirmDeleteModal
onDeleteItems={onDeleteItems}
results={items}
isOpen={isDeleteModalOpen}
onDismiss={() => {}}
/>
);
expect(screen.getByRole('heading', { name: 'Delete' })).toBeInTheDocument();
expect(screen.getByText('Do you want to delete the 2 selected dashboards?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
const button = screen.getByRole('button', { name: 'Confirm Modal Danger Button' });
expect(within(button).getByText('Delete')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,71 @@
import { css } from '@emotion/css';
import React, { FC } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui';
import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions';
import { OnDeleteSelectedItems } from '../../types';
interface Props {
onDeleteItems: OnDeleteSelectedItems;
results: Map<string, Set<string>>;
isOpen: boolean;
onDismiss: () => void;
}
export const ConfirmDeleteModal: FC<Props> = ({ results, onDeleteItems, isOpen, onDismiss }) => {
const theme = useTheme();
const styles = getStyles(theme);
const dashboards = Array.from(results.get('dashboard') ?? []);
const folders = Array.from(results.get('folders') ?? []);
const folderCount = folders.length;
const dashCount = dashboards.length;
let text = 'Do you want to delete the ';
let subtitle;
const dashEnding = dashCount === 1 ? '' : 's';
const folderEnding = folderCount === 1 ? '' : 's';
if (folderCount > 0 && dashCount > 0) {
text += `selected folder${folderEnding} and dashboard${dashEnding}?\n`;
subtitle = `All dashboards and alerts of the selected folder${folderEnding} will also be deleted`;
} else if (folderCount > 0) {
text += `selected folder${folderEnding} and all ${folderCount === 1 ? 'its' : 'their'} dashboards and alerts?`;
} else {
text += `${dashCount} selected dashboard${dashEnding}?`;
}
const deleteItems = () => {
deleteFoldersAndDashboards(folders, dashboards).then(() => {
onDismiss();
onDeleteItems(folders, dashboards);
});
};
return isOpen ? (
<ConfirmModal
isOpen={isOpen}
title="Delete"
body={
<>
{text} {subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</>
}
confirmText="Delete"
onConfirm={deleteItems}
onDismiss={onDismiss}
/>
) : null;
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
subtitle: css`
font-size: ${theme.typography.size.base};
padding-top: ${theme.spacing.md};
`,
};
});

View File

@ -1,31 +1,64 @@
import React from 'react'; import React, { useState } from 'react';
import { Button, Checkbox, HorizontalGroup, useStyles2 } from '@grafana/ui'; import { Button, Checkbox, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { FolderDTO, FolderInfo } from 'app/types';
import { GENERAL_FOLDER_UID } from '../../constants';
import { getStyles } from './ActionRow'; import { getStyles } from './ActionRow';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
import { MoveToFolderModal } from './MoveToFolderModal';
type Props = { type Props = {
items: Map<string, Set<string>>; items: Map<string, Set<string>>;
folder?: FolderDTO; // when we are loading in folder page
}; };
export function ManageActions({ items }: Props) { export function ManageActions({ items, folder }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const canMove = true; const canSave = folder?.canSave;
const canDelete = true; const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders;
const canMove = hasEditPermissionInFolders;
// TODO: check user permissions for delete, should not be able to delete if includes general folder and user don't have permissions
// There is not GENERAL_FOLDER_UID configured yet, we need to make sure to add it to the data.
const selectedFolders = Array.from(items.get('folders') ?? []);
console.log({ selectedFolders });
const includesGeneralFolder = selectedFolders.find((result) => result === GENERAL_FOLDER_UID);
const canDelete = hasEditPermissionInFolders && !includesGeneralFolder;
const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const onMove = () => { const onMove = () => {
alert('TODO, move....'); setIsMoveModalOpen(true);
}; };
const onDelete = () => { const onDelete = () => {
alert('TODO, delete....'); setIsDeleteModalOpen(true);
}; };
const onToggleAll = () => { const onToggleAll = () => {
alert('TODO, toggle all....'); alert('TODO, toggle all....');
}; };
//Todo: update item lists that were moved
const onMoveItems = (selectedDashboards: string[], folder: FolderInfo | null) => {
console.log({ selectedDashboards });
console.log({ folder });
console.log('items were moved in the backend');
};
//Todo: update item lists that were deleted
const onDeleteItems = (folders: string[], dashboards: string[]) => {
console.log({ folders });
console.log({ dashboards });
console.log('items were moved in the backend');
};
return ( return (
<div className={styles.actionRow}> <div className={styles.actionRow}>
<div className={styles.rowContainer}> <div className={styles.rowContainer}>
@ -48,6 +81,19 @@ export function ManageActions({ items }: Props) {
})} })}
</HorizontalGroup> </HorizontalGroup>
</div> </div>
<ConfirmDeleteModal
onDeleteItems={onDeleteItems}
results={items}
isOpen={isDeleteModalOpen}
onDismiss={() => setIsDeleteModalOpen(false)}
/>
<MoveToFolderModal
onMoveItems={onMoveItems}
results={items}
isOpen={isMoveModalOpen}
onDismiss={() => setIsMoveModalOpen(false)}
/>
</div> </div>
); );
} }

View File

@ -0,0 +1,37 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import { MoveToFolderModal } from './MoveToFolderModal';
jest.mock('app/core/components/Select/FolderPicker', () => {
return {
FolderPicker: () => null,
};
});
describe('MoveToFolderModal', () => {
it('should render correct title, body, dismiss-, cancel- and move-text', async () => {
const items = new Map();
const dashboardsUIDs = new Set();
dashboardsUIDs.add('uid1');
dashboardsUIDs.add('uid2');
items.set('dashboard', dashboardsUIDs);
const isMoveModalOpen = true;
const mockStore = configureMockStore<any, any>();
const store = mockStore({ dashboard: { panels: [] } });
const onMoveItems = jest.fn();
render(
<Provider store={store}>
<MoveToFolderModal onMoveItems={onMoveItems} results={items} isOpen={isMoveModalOpen} onDismiss={() => {}} />
</Provider>
);
expect(screen.getByRole('heading', { name: 'Choose Dashboard Folder' })).toBeInTheDocument();
expect(screen.getByText('Move the 2 selected dashboards to the following folder:')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
});
});

View File

@ -0,0 +1,90 @@
import { css } from '@emotion/css';
import React, { FC, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { Button, HorizontalGroup, Modal, stylesFactory, useTheme } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { useAppNotification } from 'app/core/copy/appNotification';
import { moveDashboards } from 'app/features/manage-dashboards/state/actions';
import { FolderInfo } from 'app/types';
import { OnMoveSelectedItems } from '../../types';
interface Props {
onMoveItems: OnMoveSelectedItems;
results: Map<string, Set<string>>;
isOpen: boolean;
onDismiss: () => void;
}
export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onDismiss }) => {
const [folder, setFolder] = useState<FolderInfo | null>(null);
const theme = useTheme();
const styles = getStyles(theme);
const notifyApp = useAppNotification();
const selectedDashboards = Array.from(results.get('dashboard') ?? []);
const moveTo = () => {
if (folder && selectedDashboards.length) {
const folderTitle = folder.title ?? 'General';
moveDashboards(selectedDashboards, folder).then((result: any) => {
if (result.successCount > 0) {
const ending = result.successCount === 1 ? '' : 's';
const header = `Dashboard${ending} Moved`;
const msg = `${result.successCount} dashboard${ending} moved to ${folderTitle}`;
notifyApp.success(header, msg);
}
if (result.totalCount === result.alreadyInFolderCount) {
notifyApp.error('Error', `Dashboard already belongs to folder ${folderTitle}`);
} else {
//update the list
onMoveItems(selectedDashboards, folder);
}
onDismiss();
});
}
};
return isOpen ? (
<Modal
className={styles.modal}
title="Choose Dashboard Folder"
icon="folder-plus"
isOpen={isOpen}
onDismiss={onDismiss}
>
<>
<div className={styles.content}>
<p>
Move the {selectedDashboards.length} selected dashboard{selectedDashboards.length === 1 ? '' : 's'} to the
following folder:
</p>
<FolderPicker onChange={(f) => setFolder(f as FolderInfo)} />
</div>
<HorizontalGroup justify="center">
<Button variant="primary" onClick={moveTo}>
Move
</Button>
<Button variant="secondary" onClick={onDismiss}>
Cancel
</Button>
</HorizontalGroup>
</>
</Modal>
) : null;
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
modal: css`
width: 500px;
`,
content: css`
margin-bottom: ${theme.spacing.lg};
`,
};
});

View File

@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-undef */
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table'; import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table';

View File

@ -108,3 +108,7 @@ export interface SearchQueryParams {
layout?: SearchLayout | null; layout?: SearchLayout | null;
folder?: string | null; folder?: string | null;
} }
// new Search Types
export type OnDeleteSelectedItems = (folders: string[], dashboards: string[]) => void;
export type OnMoveSelectedItems = (selectedDashboards: string[], folder: FolderInfo | null) => void;