Search: manage selection in state (#48793)
This commit is contained in:
@@ -16,7 +16,7 @@ export const TableCell: FC<Props> = ({ cell, tableStyles, onCellFilterAdded, col
|
||||
const cellProps = cell.getCellProps();
|
||||
const field = (cell.column as any as GrafanaTableColumn).field;
|
||||
|
||||
if (!field.display) {
|
||||
if (!field?.display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FixedSizeGrid } from 'react-window';
|
||||
|
||||
import { DataFrameView, GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField } from '@grafana/ui';
|
||||
import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Button } from '@grafana/ui';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||
|
||||
@@ -17,8 +17,10 @@ import { getGrafanaSearcher, QueryFilters, QueryResult } from '../service';
|
||||
import { getTermCounts } from '../service/backend';
|
||||
import { DashboardSearchItemType, DashboardSectionItem, SearchLayout } from '../types';
|
||||
|
||||
import { ActionRow } from './components/ActionRow';
|
||||
import { ActionRow, getValidQueryLayout } from './components/ActionRow';
|
||||
import { ManageActions } from './components/ManageActions';
|
||||
import { SearchResultsTable } from './components/SearchResultsTable';
|
||||
import { newSearchSelection, updateSearchSelection } from './selection';
|
||||
|
||||
const node: NavModelItem = {
|
||||
id: 'search',
|
||||
@@ -35,6 +37,8 @@ export default function SearchPage() {
|
||||
);
|
||||
const [showManage, setShowManage] = useState(false); // grid vs list view
|
||||
|
||||
const [searchSelection, setSearchSelection] = useState(newSearchSelection());
|
||||
|
||||
const results = useAsync(() => {
|
||||
const { query: searchQuery, tag: tags, datasource } = query;
|
||||
|
||||
@@ -71,7 +75,127 @@ export default function SearchPage() {
|
||||
onTagFilterChange([...new Set(query.tag as string[]).add(tag)]);
|
||||
};
|
||||
|
||||
const showPreviews = query.layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews;
|
||||
const toggleSelection = (kind: string, uid: string) => {
|
||||
const current = searchSelection.isSelected(kind, uid);
|
||||
if (kind === 'folder') {
|
||||
// ??? also select all children?
|
||||
}
|
||||
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
|
||||
};
|
||||
|
||||
const layout = getValidQueryLayout(query);
|
||||
const showPreviews = layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews;
|
||||
|
||||
const renderResults = () => {
|
||||
if (results.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const df = results.value?.body;
|
||||
if (!df || !df.length) {
|
||||
return (
|
||||
<div className={styles.noResults}>
|
||||
<div>No results found for your query.</div>
|
||||
<br />
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (query.query) {
|
||||
onQueryChange('');
|
||||
}
|
||||
if (query.tag?.length) {
|
||||
onTagFilterChange([]);
|
||||
}
|
||||
if (query.datasource) {
|
||||
onDatasourceChange(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove search constraints
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoSizer style={{ width: '100%', height: '700px' }}>
|
||||
{({ width, height }) => {
|
||||
if (showPreviews) {
|
||||
const view = new DataFrameView<QueryResult>(df);
|
||||
|
||||
// Hacked to reuse existing SearchCard (and old DashboardSectionItem)
|
||||
const itemProps = {
|
||||
editable: showManage,
|
||||
onToggleChecked: (item: any) => {
|
||||
const d = item as DashboardSectionItem;
|
||||
const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
|
||||
toggleSelection(t, d.uid!);
|
||||
},
|
||||
onTagSelected,
|
||||
};
|
||||
|
||||
const numColumns = Math.ceil(width / 320);
|
||||
const cellWidth = width / numColumns;
|
||||
const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
|
||||
const numRows = Math.ceil(df.length / numColumns);
|
||||
return (
|
||||
<FixedSizeGrid
|
||||
columnCount={numColumns}
|
||||
columnWidth={cellWidth}
|
||||
rowCount={numRows}
|
||||
rowHeight={cellHeight}
|
||||
className={styles.wrapper}
|
||||
innerElementType="ul"
|
||||
height={height}
|
||||
width={width - 2}
|
||||
>
|
||||
{({ columnIndex, rowIndex, style }) => {
|
||||
const index = rowIndex * numColumns + columnIndex;
|
||||
const item = view.get(index);
|
||||
const kind = item.kind ?? 'dashboard';
|
||||
const facade: DashboardSectionItem = {
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
checked: searchSelection.isSelected(kind, item.uid),
|
||||
};
|
||||
|
||||
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
|
||||
// And without this wrapper there is no room for that margin
|
||||
return item ? (
|
||||
<li style={style} className={styles.virtualizedGridItemWrapper}>
|
||||
<SearchCard key={item.uid} {...itemProps} item={facade} />
|
||||
</li>
|
||||
) : null;
|
||||
}}
|
||||
</FixedSizeGrid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchResultsTable
|
||||
data={df}
|
||||
selection={showManage ? searchSelection.isSelected : undefined}
|
||||
selectionToggle={toggleSelection}
|
||||
layout={layout}
|
||||
width={width - 5}
|
||||
height={height}
|
||||
tags={query.tag}
|
||||
onTagFilterChange={onTagChange}
|
||||
onDatasourceChange={onDatasourceChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page navModel={{ node: node, main: node }}>
|
||||
@@ -84,113 +208,42 @@ export default function SearchPage() {
|
||||
placeholder="Search for dashboards and panels"
|
||||
/>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Show the manage options">
|
||||
<InlineField label="Show manage options">
|
||||
<InlineSwitch value={showManage} onChange={() => setShowManage(!showManage)} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<br />
|
||||
{results.loading && <Spinner />}
|
||||
{results.value?.body && (
|
||||
<div>
|
||||
<ActionRow
|
||||
onLayoutChange={(v) => {
|
||||
if (v === SearchLayout.Folders) {
|
||||
if (query.query) {
|
||||
onQueryChange(''); // parent will clear the sort
|
||||
}
|
||||
<hr />
|
||||
|
||||
{Boolean(searchSelection.items.size > 0) ? (
|
||||
<ManageActions items={searchSelection.items} />
|
||||
) : (
|
||||
<ActionRow
|
||||
onLayoutChange={(v) => {
|
||||
if (v === SearchLayout.Folders) {
|
||||
if (query.query) {
|
||||
onQueryChange(''); // parent will clear the sort
|
||||
}
|
||||
onLayoutChange(v);
|
||||
}}
|
||||
onSortChange={onSortChange}
|
||||
onTagFilterChange={onTagFilterChange}
|
||||
getTagOptions={getTagOptions}
|
||||
onDatasourceChange={onDatasourceChange}
|
||||
query={query}
|
||||
/>
|
||||
|
||||
<PreviewsSystemRequirements
|
||||
bottomSpacing={3}
|
||||
showPreviews={showPreviews}
|
||||
onRemove={() => onLayoutChange(SearchLayout.List)}
|
||||
/>
|
||||
|
||||
<AutoSizer style={{ width: '100%', height: '700px' }}>
|
||||
{({ width, height }) => {
|
||||
if (showPreviews) {
|
||||
const df = results.value?.body!;
|
||||
const view = new DataFrameView<QueryResult>(df);
|
||||
|
||||
// HACK for grid view
|
||||
const itemProps = {
|
||||
editable: showManage,
|
||||
onToggleChecked: (v: any) => {
|
||||
console.log('CHECKED?', v);
|
||||
},
|
||||
onTagSelected,
|
||||
};
|
||||
|
||||
const numColumns = Math.ceil(width / 320);
|
||||
const cellWidth = width / numColumns;
|
||||
const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
|
||||
const numRows = Math.ceil(df.length / numColumns);
|
||||
return (
|
||||
<FixedSizeGrid
|
||||
columnCount={numColumns}
|
||||
columnWidth={cellWidth}
|
||||
rowCount={numRows}
|
||||
rowHeight={cellHeight}
|
||||
className={styles.wrapper}
|
||||
innerElementType="ul"
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
{({ columnIndex, rowIndex, style }) => {
|
||||
const index = rowIndex * numColumns + columnIndex;
|
||||
const item = view.get(index);
|
||||
const facade: DashboardSectionItem = {
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type:
|
||||
item.kind === 'folder'
|
||||
? DashboardSearchItemType.DashFolder
|
||||
: DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
};
|
||||
|
||||
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
|
||||
// And without this wrapper there is no room for that margin
|
||||
return item ? (
|
||||
<li style={style} className={styles.virtualizedGridItemWrapper}>
|
||||
<SearchCard key={item.uid} {...itemProps} item={facade} />
|
||||
</li>
|
||||
) : null;
|
||||
}}
|
||||
</FixedSizeGrid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchResultsTable
|
||||
data={results.value!.body}
|
||||
showCheckbox={showManage}
|
||||
layout={query.layout}
|
||||
width={width}
|
||||
height={height}
|
||||
tags={query.tag}
|
||||
onTagFilterChange={onTagChange}
|
||||
onDatasourceChange={onDatasourceChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
}
|
||||
onLayoutChange(v);
|
||||
}}
|
||||
onSortChange={onSortChange}
|
||||
onTagFilterChange={onTagFilterChange}
|
||||
getTagOptions={getTagOptions}
|
||||
onDatasourceChange={onDatasourceChange}
|
||||
query={query}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPreviews && (
|
||||
<PreviewsSystemRequirements
|
||||
bottomSpacing={3}
|
||||
showPreviews={showPreviews}
|
||||
onRemove={() => onLayoutChange(SearchLayout.List)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{renderResults()}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
@@ -205,7 +258,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
`,
|
||||
|
||||
virtualizedGridItemWrapper: css`
|
||||
padding: 4px;
|
||||
`,
|
||||
@@ -217,4 +269,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
list-style: none;
|
||||
}
|
||||
`,
|
||||
noResults: css`
|
||||
padding: ${theme.v1.spacing.md};
|
||||
background: ${theme.v1.colors.bg2};
|
||||
font-style: italic;
|
||||
margin-top: ${theme.v1.spacing.md};
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ interface Props {
|
||||
hideLayout?: boolean;
|
||||
}
|
||||
|
||||
function getValidQueryLayout(q: DashboardQuery): SearchLayout {
|
||||
export function getValidQueryLayout(q: DashboardQuery): SearchLayout {
|
||||
// Folders is not valid when a query exists
|
||||
if (q.layout === SearchLayout.Folders) {
|
||||
if (q.query || q.sort) {
|
||||
@@ -82,7 +82,7 @@ export const ActionRow: FC<Props> = ({
|
||||
|
||||
ActionRow.displayName = 'ActionRow';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
actionRow: css`
|
||||
display: none;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Checkbox, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getStyles } from './ActionRow';
|
||||
|
||||
type Props = {
|
||||
items: Map<string, Set<string>>;
|
||||
};
|
||||
|
||||
export function ManageActions({ items }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const canMove = true;
|
||||
const canDelete = true;
|
||||
|
||||
const onMove = () => {
|
||||
alert('TODO, move....');
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
alert('TODO, delete....');
|
||||
};
|
||||
|
||||
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} />
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import React, { useMemo } from 'react';
|
||||
import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
import { DataFrame, DataFrameType, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataFrame, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
|
||||
import { getTableStyles } from '@grafana/ui/src/components/Table/styles';
|
||||
|
||||
import { LocationInfo } from '../../service';
|
||||
import { SearchLayout } from '../../types';
|
||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||
|
||||
import { generateColumns } from './columns';
|
||||
|
||||
@@ -17,7 +18,8 @@ type Props = {
|
||||
data: DataFrame;
|
||||
width: number;
|
||||
height: number;
|
||||
showCheckbox: boolean;
|
||||
selection?: SelectionChecker;
|
||||
selectionToggle?: SelectionToggle;
|
||||
layout: SearchLayout;
|
||||
tags: string[];
|
||||
onTagFilterChange: (tags: string[]) => void;
|
||||
@@ -51,7 +53,8 @@ export const SearchResultsTable = ({
|
||||
width,
|
||||
height,
|
||||
tags,
|
||||
showCheckbox,
|
||||
selection,
|
||||
selectionToggle,
|
||||
layout,
|
||||
onTagFilterChange,
|
||||
onDatasourceChange,
|
||||
@@ -72,18 +75,19 @@ export const SearchResultsTable = ({
|
||||
// React-table column definitions
|
||||
const access = useMemo(() => new DataFrameView<FieldAccess>(data), [data]);
|
||||
const memoizedColumns = useMemo(() => {
|
||||
const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing || layout === SearchLayout.Folders;
|
||||
const isDashboardList = layout === SearchLayout.Folders;
|
||||
return generateColumns(
|
||||
access,
|
||||
isDashboardList,
|
||||
width,
|
||||
showCheckbox,
|
||||
selection,
|
||||
selectionToggle,
|
||||
styles,
|
||||
tags,
|
||||
onTagFilterChange,
|
||||
onDatasourceChange
|
||||
);
|
||||
}, [data.meta?.type, layout, access, width, styles, tags, showCheckbox, onTagFilterChange, onDatasourceChange]);
|
||||
}, [layout, access, width, styles, tags, selection, selectionToggle, onTagFilterChange, onDatasourceChange]);
|
||||
|
||||
const options: TableOptions<{}> = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
|
||||
import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell';
|
||||
|
||||
import { LocationInfo } from '../../service';
|
||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||
|
||||
import { FieldAccess, TableColumn } from './SearchResultsTable';
|
||||
|
||||
@@ -14,7 +15,8 @@ export const generateColumns = (
|
||||
data: DataFrameView<FieldAccess>,
|
||||
isDashboardList: boolean,
|
||||
availableWidth: number,
|
||||
showCheckbox: boolean,
|
||||
selection: SelectionChecker | undefined,
|
||||
selectionToggle: SelectionToggle | undefined,
|
||||
styles: { [key: string]: string },
|
||||
tags: string[],
|
||||
onTagFilterChange: (tags: string[]) => void,
|
||||
@@ -22,13 +24,14 @@ export const generateColumns = (
|
||||
): TableColumn[] => {
|
||||
const columns: TableColumn[] = [];
|
||||
const uidField = data.fields.uid!;
|
||||
const kindField = data.fields.kind!;
|
||||
const access = data.fields;
|
||||
|
||||
availableWidth -= 8; // ???
|
||||
let width = 50;
|
||||
|
||||
// TODO: Add optional checkbox support
|
||||
if (showCheckbox) {
|
||||
if (selection && selectionToggle) {
|
||||
width = 30;
|
||||
columns.push({
|
||||
id: `column-checkbox`,
|
||||
Header: () => (
|
||||
@@ -37,20 +40,25 @@ export const generateColumns = (
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log('SELECT ALL!!!', e);
|
||||
alert('SELECT ALL!!!');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
width: 30,
|
||||
width,
|
||||
Cell: (p) => {
|
||||
const uid = uidField.values.get(p.row.index);
|
||||
const kind = kindField ? kindField.values.get(p.row.index) : 'dashboard'; // HACK for now
|
||||
const selected = selection(kind, uid);
|
||||
const hasUID = uid != null; // Panels don't have UID! Likely should not be shown on pages with manage options
|
||||
return (
|
||||
<div {...p.cellProps} className={p.cellStyle}>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox
|
||||
disabled={!hasUID}
|
||||
value={selected && hasUID}
|
||||
onChange={(e) => {
|
||||
console.log('SELECTED!!!', uid);
|
||||
selectionToggle(kind, uid);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -260,10 +268,10 @@ function makeTypeColumn(
|
||||
return {
|
||||
Cell: DefaultCell,
|
||||
id: `column-type`,
|
||||
field: kindField,
|
||||
field: kindField ?? typeField,
|
||||
Header: 'Type',
|
||||
accessor: (row: any, i: number) => {
|
||||
const kind = kindField.values.get(i);
|
||||
const kind = kindField?.values.get(i) ?? 'dashboard';
|
||||
let icon = 'public/img/icons/unicons/apps.svg';
|
||||
let txt = 'Dashboard';
|
||||
if (kind) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { newSearchSelection, updateSearchSelection } from './selection';
|
||||
|
||||
describe('Search selection helper', () => {
|
||||
it('simple dashboard selection', () => {
|
||||
let sel = newSearchSelection();
|
||||
expect(sel.isSelected('dash', 'aaa')).toBe(false);
|
||||
|
||||
sel = updateSearchSelection(sel, true, 'dash', ['aaa']);
|
||||
expect(sel.isSelected('dash', 'aaa')).toBe(true);
|
||||
|
||||
sel = updateSearchSelection(sel, false, 'dash', ['aaa']);
|
||||
expect(sel.isSelected('dash', 'aaa')).toBe(false);
|
||||
expect(sel.items).toMatchInlineSnapshot(`Map {}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
export type SelectionChecker = (kind: string, uid: string) => boolean;
|
||||
export type SelectionToggle = (kind: string, uid: string) => void;
|
||||
|
||||
export interface SearchSelection {
|
||||
// Check if an item is selected
|
||||
isSelected: SelectionChecker;
|
||||
|
||||
// Selected items by kind
|
||||
items: Map<string, Set<string>>;
|
||||
}
|
||||
|
||||
export function newSearchSelection(): SearchSelection {
|
||||
// the check is called often, on potentially large (all) results so using Map/Set is better than simple array
|
||||
const items = new Map<string, Set<string>>();
|
||||
|
||||
const isSelected = (kind: string, uid: string) => {
|
||||
return Boolean(items.get(kind)?.has(uid));
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
isSelected,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSearchSelection(
|
||||
old: SearchSelection,
|
||||
selected: boolean,
|
||||
kind: string,
|
||||
uids: string[]
|
||||
): SearchSelection {
|
||||
const items = old.items; // mutate! :/
|
||||
|
||||
if (uids.length) {
|
||||
const k = items.get(kind);
|
||||
if (k) {
|
||||
for (const uid of uids) {
|
||||
if (selected) {
|
||||
k.add(uid);
|
||||
} else {
|
||||
k.delete(uid);
|
||||
}
|
||||
}
|
||||
if (k.size < 1) {
|
||||
items.delete(kind);
|
||||
}
|
||||
} else if (selected) {
|
||||
items.set(kind, new Set<string>(uids));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
isSelected: (kind: string, uid: string) => {
|
||||
return Boolean(items.get(kind)?.has(uid));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -173,6 +173,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
const found = this.index!.search(query);
|
||||
|
||||
// frame fields
|
||||
const uid: string[] = [];
|
||||
const url: string[] = [];
|
||||
const kind: string[] = [];
|
||||
const type: string[] = [];
|
||||
@@ -195,6 +196,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
uid.push(input.uid?.get(index)!);
|
||||
url.push(input.url?.get(index) ?? '?');
|
||||
location.push(input.location?.get(index) as any);
|
||||
datasource.push(input.datasource?.get(index) as any);
|
||||
@@ -206,6 +208,7 @@ export class MiniSearcher implements GrafanaSearcher {
|
||||
score.push(res.score);
|
||||
}
|
||||
const fields: Field[] = [
|
||||
{ name: 'uid', config: {}, type: FieldType.string, values: new ArrayVector(uid) },
|
||||
{ name: 'kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) },
|
||||
{ name: 'name', config: {}, type: FieldType.string, values: new ArrayVector(name) },
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('simple search', () => {
|
||||
|
||||
const searcher = new MiniSearcher(supplier);
|
||||
let results = await searcher.search('name');
|
||||
expect(results.body.fields[1].values.toArray()).toMatchInlineSnapshot(`
|
||||
expect(results.body.fields[2].values.toArray()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"A name (dash)",
|
||||
"B name (dash)",
|
||||
@@ -37,7 +37,7 @@ describe('simple search', () => {
|
||||
`);
|
||||
|
||||
results = await searcher.search('B');
|
||||
expect(results.body.fields[1].values.toArray()).toMatchInlineSnapshot(`
|
||||
expect(results.body.fields[2].values.toArray()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"B name (dash)",
|
||||
"B name (panels)",
|
||||
|
||||
Reference in New Issue
Block a user