Search: implement clear-selection and select all buttons (#49363)

This commit is contained in:
Ryan McKinley 2022-05-23 10:01:18 -07:00 committed by GitHub
parent f9d1d8370f
commit 653c82cec4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 35 deletions

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Button, Checkbox, HorizontalGroup, useStyles2 } from '@grafana/ui';
import { Button, HorizontalGroup, IconButton, IconName, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { FolderDTO } from 'app/types';
@ -15,9 +15,10 @@ type Props = {
items: Map<string, Set<string>>;
folder?: FolderDTO; // when we are loading in folder page
onChange: OnMoveOrDeleleSelectedItems;
clearSelection: () => void;
};
export function ManageActions({ items, folder, onChange }: Props) {
export function ManageActions({ items, folder, onChange, clearSelection }: Props) {
const styles = useStyles2(getStyles);
const canSave = folder?.canSave;
@ -43,30 +44,17 @@ export function ManageActions({ items, folder, onChange }: Props) {
setIsDeleteModalOpen(true);
};
const onToggleAll = () => {
alert('TODO, toggle all....');
};
return (
<div className={styles.actionRow}>
<div className={styles.rowContainer}>
<HorizontalGroup spacing="md" width="auto">
<Checkbox value={false} onClick={onToggleAll} />
<IconButton name={'check-square' as IconName} onClick={clearSelection} title="Uncheck everything" />
<Button disabled={!canMove} onClick={onMove} icon="exchange-alt" variant="secondary">
Move
</Button>
<Button disabled={!canDelete} onClick={onDelete} icon="trash-alt" variant="destructive">
Delete
</Button>
{[...items.keys()].map((k) => {
const vals = items.get(k);
return (
<div key={k}>
{k} ({vals?.size})
</div>
);
})}
</HorizontalGroup>
</div>

View File

@ -21,6 +21,7 @@ export type SearchResultsProps = {
height: number;
selection?: SelectionChecker;
selectionToggle?: SelectionToggle;
clearSelection: () => void;
onTagSelected: (tag: string) => void;
onDatasourceChange?: (datasource?: string) => void;
};
@ -32,7 +33,16 @@ export type TableColumn = Column & {
const HEADER_HEIGHT = 36; // pixels
export const SearchResultsTable = React.memo(
({ response, width, height, selection, selectionToggle, onTagSelected, onDatasourceChange }: SearchResultsProps) => {
({
response,
width,
height,
selection,
selectionToggle,
clearSelection,
onTagSelected,
onDatasourceChange,
}: SearchResultsProps) => {
const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getTableStyles);
@ -48,8 +58,17 @@ export const SearchResultsTable = React.memo(
// React-table column definitions
const memoizedColumns = useMemo(() => {
return generateColumns(response, width, selection, selectionToggle, styles, onTagSelected, onDatasourceChange);
}, [response, width, styles, selection, selectionToggle, onTagSelected, onDatasourceChange]);
return generateColumns(
response,
width,
selection,
selectionToggle,
clearSelection,
styles,
onTagSelected,
onDatasourceChange
);
}, [response, width, styles, selection, selectionToggle, clearSelection, onTagSelected, onDatasourceChange]);
const options: TableOptions<{}> = useMemo(
() => ({

View File

@ -54,12 +54,14 @@ export const SearchView = ({ showManage, folderDTO, queryText, hidePseudoFolders
return getGrafanaSearcher().search(q);
}, [query, layout, queryText, folderDTO]);
const clearSelection = useCallback(() => {
searchSelection.items.clear();
setSearchSelection({ ...searchSelection });
}, [searchSelection]);
const toggleSelection = useCallback(
(kind: string, uid: string) => {
const current = searchSelection.isSelected(kind, uid);
if (kind === 'folder') {
// ??? also select all children?
}
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
},
[searchSelection]
@ -151,6 +153,7 @@ export const SearchView = ({ showManage, folderDTO, queryText, hidePseudoFolders
response: value!,
selection,
selectionToggle: toggleSelection,
clearSelection,
width: width,
height: height,
onTagSelected: onTagAdd,
@ -175,7 +178,7 @@ export const SearchView = ({ showManage, folderDTO, queryText, hidePseudoFolders
return (
<>
{Boolean(searchSelection.items.size > 0) ? (
<ManageActions items={searchSelection.items} onChange={onChangeItemsList} />
<ManageActions items={searchSelection.items} onChange={onChangeItemsList} clearSelection={clearSelection} />
) : (
<ActionRow
onLayoutChange={(v) => {

View File

@ -4,7 +4,7 @@ import SVG from 'react-inlinesvg';
import { Field } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
import { Checkbox, Icon, IconButton, IconName, TagList } from '@grafana/ui';
import { QueryResponse, SearchResultMeta } from '../../service';
import { SelectionChecker, SelectionToggle } from '../selection';
@ -19,6 +19,7 @@ export const generateColumns = (
availableWidth: number,
selection: SelectionChecker | undefined,
selectionToggle: SelectionToggle | undefined,
clearSelection: () => void,
styles: { [key: string]: string },
onTagSelected: (tag: string) => void,
onDatasourceChange?: (datasource?: string) => void
@ -35,17 +36,36 @@ export const generateColumns = (
columns.push({
id: `column-checkbox`,
width,
Header: () => (
Header: () => {
if (selection('*', '*')) {
return (
<div className={styles.checkboxHeader}>
<IconButton name={'check-square' as any} onClick={clearSelection} />
</div>
);
}
return (
<div className={styles.checkboxHeader}>
<Checkbox
checked={false}
onChange={(e) => {
e.stopPropagation();
e.preventDefault();
alert('SELECT ALL!!!');
const { view } = response;
const count = Math.min(view.length, 50);
for (let i = 0; i < count; i++) {
const item = view.get(i);
if (item.uid && item.kind) {
if (!selection(item.kind, item.uid)) {
selectionToggle(item.kind, item.uid);
}
}
}
}}
/>
</div>
),
);
},
Cell: (p) => {
const uid = uidField.values.get(p.row.index);
const kind = kindField ? kindField.values.get(p.row.index) : 'dashboard'; // HACK for now

View File

@ -7,9 +7,13 @@ describe('Search selection helper', () => {
sel = updateSearchSelection(sel, true, 'dash', ['aaa']);
expect(sel.isSelected('dash', 'aaa')).toBe(true);
expect(sel.isSelected('dash', '*')).toBeTruthy();
expect(sel.isSelected('alert', '*')).toBeFalsy();
expect(sel.isSelected('*', '*')).toBeTruthy();
sel = updateSearchSelection(sel, false, 'dash', ['aaa']);
expect(sel.isSelected('dash', 'aaa')).toBe(false);
expect(sel.items).toMatchInlineSnapshot(`Map {}`);
expect(sel.isSelected('*', '*')).toBeFalsy();
});
});

View File

@ -1,3 +1,4 @@
// Using '*' for uid will return true if anything is selected
export type SelectionChecker = (kind: string, uid: string) => boolean;
export type SelectionToggle = (kind: string, uid: string) => void;
@ -52,6 +53,17 @@ export function updateSearchSelection(
return {
items,
isSelected: (kind: string, uid: string) => {
if (uid === '*') {
if (kind === '*') {
for (const k of items.keys()) {
if (items.get(k)?.size) {
return true;
}
}
return false;
}
return Boolean(items.get(kind)?.size);
}
return Boolean(items.get(kind)?.has(uid));
},
};