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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import SVG from 'react-inlinesvg';
import { Field } from '@grafana/data'; import { Field } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime'; 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 { QueryResponse, SearchResultMeta } from '../../service';
import { SelectionChecker, SelectionToggle } from '../selection'; import { SelectionChecker, SelectionToggle } from '../selection';
@ -19,6 +19,7 @@ export const generateColumns = (
availableWidth: number, availableWidth: number,
selection: SelectionChecker | undefined, selection: SelectionChecker | undefined,
selectionToggle: SelectionToggle | undefined, selectionToggle: SelectionToggle | undefined,
clearSelection: () => void,
styles: { [key: string]: string }, styles: { [key: string]: string },
onTagSelected: (tag: string) => void, onTagSelected: (tag: string) => void,
onDatasourceChange?: (datasource?: string) => void onDatasourceChange?: (datasource?: string) => void
@ -35,17 +36,36 @@ export const generateColumns = (
columns.push({ columns.push({
id: `column-checkbox`, id: `column-checkbox`,
width, width,
Header: () => ( Header: () => {
<div className={styles.checkboxHeader}> if (selection('*', '*')) {
<Checkbox return (
onChange={(e) => { <div className={styles.checkboxHeader}>
e.stopPropagation(); <IconButton name={'check-square' as any} onClick={clearSelection} />
e.preventDefault(); </div>
alert('SELECT ALL!!!'); );
}} }
/> return (
</div> <div className={styles.checkboxHeader}>
), <Checkbox
checked={false}
onChange={(e) => {
e.stopPropagation();
e.preventDefault();
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) => { Cell: (p) => {
const uid = uidField.values.get(p.row.index); const uid = uidField.values.get(p.row.index);
const kind = kindField ? kindField.values.get(p.row.index) : 'dashboard'; // HACK for now 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']); sel = updateSearchSelection(sel, true, 'dash', ['aaa']);
expect(sel.isSelected('dash', 'aaa')).toBe(true); 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']); sel = updateSearchSelection(sel, false, 'dash', ['aaa']);
expect(sel.isSelected('dash', 'aaa')).toBe(false); expect(sel.isSelected('dash', 'aaa')).toBe(false);
expect(sel.items).toMatchInlineSnapshot(`Map {}`); 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 SelectionChecker = (kind: string, uid: string) => boolean;
export type SelectionToggle = (kind: string, uid: string) => void; export type SelectionToggle = (kind: string, uid: string) => void;
@ -52,6 +53,17 @@ export function updateSearchSelection(
return { return {
items, items,
isSelected: (kind: string, uid: string) => { 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)); return Boolean(items.get(kind)?.has(uid));
}, },
}; };