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 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';
|
||||||
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 { 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';
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user