From 653c82cec4cd962ed7000ac02a413bbde1c244c9 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 23 May 2022 10:01:18 -0700 Subject: [PATCH] Search: implement clear-selection and select all buttons (#49363) --- .../search/page/components/ManageActions.tsx | 20 ++------- .../page/components/SearchResultsTable.tsx | 25 +++++++++-- .../search/page/components/SearchView.tsx | 11 +++-- .../search/page/components/columns.tsx | 44 ++++++++++++++----- .../features/search/page/selection.test.ts | 4 ++ public/app/features/search/page/selection.ts | 12 +++++ 6 files changed, 81 insertions(+), 35 deletions(-) diff --git a/public/app/features/search/page/components/ManageActions.tsx b/public/app/features/search/page/components/ManageActions.tsx index eeb2f96bccc..dedca77c017 100644 --- a/public/app/features/search/page/components/ManageActions.tsx +++ b/public/app/features/search/page/components/ManageActions.tsx @@ -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>; 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 (
- + - - {[...items.keys()].map((k) => { - const vals = items.get(k); - return ( -
- {k} ({vals?.size}) -
- ); - })}
diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index b1a19cbec3b..235fd71c1e4 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -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( () => ({ diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index 0cfd2b42d14..cfa6302f01a 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -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) ? ( - + ) : ( { diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index d3eb227dd6a..97088bcfd53 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -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: () => ( -
- { - e.stopPropagation(); - e.preventDefault(); - alert('SELECT ALL!!!'); - }} - /> -
- ), + Header: () => { + if (selection('*', '*')) { + return ( +
+ +
+ ); + } + return ( +
+ { + 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); + } + } + } + }} + /> +
+ ); + }, Cell: (p) => { const uid = uidField.values.get(p.row.index); const kind = kindField ? kindField.values.get(p.row.index) : 'dashboard'; // HACK for now diff --git a/public/app/features/search/page/selection.test.ts b/public/app/features/search/page/selection.test.ts index 9045b7b4e91..c8d092b6b49 100644 --- a/public/app/features/search/page/selection.test.ts +++ b/public/app/features/search/page/selection.test.ts @@ -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(); }); }); diff --git a/public/app/features/search/page/selection.ts b/public/app/features/search/page/selection.ts index 3902b3c2d5e..121399ee448 100644 --- a/public/app/features/search/page/selection.ts +++ b/public/app/features/search/page/selection.ts @@ -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)); }, };