mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search: implement clear-selection and select all buttons (#49363)
This commit is contained in:
parent
f9d1d8370f
commit
653c82cec4
@ -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>
|
||||
|
||||
|
@ -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(
|
||||
() => ({
|
||||
|
@ -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) => {
|
||||
|
@ -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: () => (
|
||||
<div className={styles.checkboxHeader}>
|
||||
<Checkbox
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
alert('SELECT ALL!!!');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
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();
|
||||
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
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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));
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user