mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search (Playground) Implement Delete and Move actions in New Search (#48863)
This commit is contained in:
parent
270e38cfcf
commit
d5a881598f
@ -5,5 +5,6 @@ export const SEARCH_ITEM_MARGIN = 8;
|
||||
export const DEFAULT_SORT = { label: 'A\u2013Z', value: 'alpha-asc' };
|
||||
export const SECTION_STORAGE_KEY = 'search.sections';
|
||||
export const GENERAL_FOLDER_ID = 0;
|
||||
export const GENERAL_FOLDER_UID = 'GeneralFolderUID';
|
||||
export const GENERAL_FOLDER_TITLE = 'General';
|
||||
export const PREVIEWS_LOCAL_STORAGE_KEY = 'grafana.dashboard.previews';
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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};
|
||||
`,
|
||||
};
|
||||
});
|
@ -1,31 +1,64 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
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 { ConfirmDeleteModal } from './ConfirmDeleteModal';
|
||||
import { MoveToFolderModal } from './MoveToFolderModal';
|
||||
|
||||
type Props = {
|
||||
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 canMove = true;
|
||||
const canDelete = true;
|
||||
const canSave = folder?.canSave;
|
||||
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 = () => {
|
||||
alert('TODO, move....');
|
||||
setIsMoveModalOpen(true);
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
alert('TODO, delete....');
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const onToggleAll = () => {
|
||||
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 (
|
||||
<div className={styles.actionRow}>
|
||||
<div className={styles.rowContainer}>
|
||||
@ -48,6 +81,19 @@ export function ManageActions({ items }: Props) {
|
||||
})}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
onDeleteItems={onDeleteItems}
|
||||
results={items}
|
||||
isOpen={isDeleteModalOpen}
|
||||
onDismiss={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
<MoveToFolderModal
|
||||
onMoveItems={onMoveItems}
|
||||
results={items}
|
||||
isOpen={isMoveModalOpen}
|
||||
onDismiss={() => setIsMoveModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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};
|
||||
`,
|
||||
};
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table';
|
||||
|
@ -108,3 +108,7 @@ export interface SearchQueryParams {
|
||||
layout?: SearchLayout | null;
|
||||
folder?: string | null;
|
||||
}
|
||||
|
||||
// new Search Types
|
||||
export type OnDeleteSelectedItems = (folders: string[], dashboards: string[]) => void;
|
||||
export type OnMoveSelectedItems = (selectedDashboards: string[], folder: FolderInfo | null) => void;
|
||||
|
Loading…
Reference in New Issue
Block a user