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