Search: add actions row header to new search layout (#48735)
This commit is contained in:
@@ -1,30 +1,39 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { FixedSizeGrid } from 'react-window';
|
||||||
|
|
||||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
import { DataFrameView, GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { Input, useStyles2, Spinner, Button } from '@grafana/ui';
|
import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField } from '@grafana/ui';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter';
|
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||||
|
|
||||||
|
import { PreviewsSystemRequirements } from '../components/PreviewsSystemRequirements';
|
||||||
|
import { SearchCard } from '../components/SearchCard';
|
||||||
import { useSearchQuery } from '../hooks/useSearchQuery';
|
import { useSearchQuery } from '../hooks/useSearchQuery';
|
||||||
import { getGrafanaSearcher, QueryFilters } from '../service';
|
import { getGrafanaSearcher, QueryFilters, QueryResult } from '../service';
|
||||||
import { getTermCounts } from '../service/backend';
|
import { getTermCounts } from '../service/backend';
|
||||||
|
import { DashboardSearchItemType, DashboardSectionItem, SearchLayout } from '../types';
|
||||||
|
|
||||||
import { Table } from './table/Table';
|
import { ActionRow } from './components/ActionRow';
|
||||||
|
import { SearchResultsTable } from './components/SearchResultsTable';
|
||||||
|
|
||||||
const node: NavModelItem = {
|
const node: NavModelItem = {
|
||||||
id: 'search',
|
id: 'search',
|
||||||
text: 'Search',
|
text: 'Search playground',
|
||||||
|
subTitle: 'The body below will eventually live inside existing UI layouts',
|
||||||
icon: 'dashboard',
|
icon: 'dashboard',
|
||||||
url: 'search',
|
url: 'search',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { query, onQueryChange, onTagFilterChange, onDatasourceChange } = useSearchQuery({});
|
const { query, onQueryChange, onTagFilterChange, onDatasourceChange, onSortChange, onLayoutChange } = useSearchQuery(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const [showManage, setShowManage] = useState(false); // grid vs list view
|
||||||
|
|
||||||
const results = useAsync(() => {
|
const results = useAsync(() => {
|
||||||
const { query: searchQuery, tag: tags, datasource } = query;
|
const { query: searchQuery, tag: tags, datasource } = query;
|
||||||
@@ -40,6 +49,7 @@ export default function SearchPage() {
|
|||||||
return <div className={styles.unsupported}>Unsupported</div>;
|
return <div className={styles.unsupported}>Unsupported</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This gets the possible tags from within the query results
|
||||||
const getTagOptions = (): Promise<TermCount[]> => {
|
const getTagOptions = (): Promise<TermCount[]> => {
|
||||||
const tags = results.value?.body.fields.find((f) => f.name === 'tags');
|
const tags = results.value?.body.fields.find((f) => f.name === 'tags');
|
||||||
|
|
||||||
@@ -57,33 +67,120 @@ export default function SearchPage() {
|
|||||||
onTagFilterChange(tags);
|
onTagFilterChange(tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onTagSelected = (tag: string) => {
|
||||||
|
onTagFilterChange([...new Set(query.tag as string[]).add(tag)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPreviews = query.layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={{ node: node, main: node }}>
|
<Page navModel={{ node: node, main: node }}>
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<Input value={query.query} onChange={onSearchQueryChange} autoFocus spellCheck={false} />
|
<Input
|
||||||
|
value={query.query}
|
||||||
|
onChange={onSearchQueryChange}
|
||||||
|
autoFocus
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="Search for dashboards and panels"
|
||||||
|
/>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Show the manage options">
|
||||||
|
<InlineSwitch value={showManage} onChange={() => setShowManage(!showManage)} />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
<br />
|
<br />
|
||||||
{results.loading && <Spinner />}
|
{results.loading && <Spinner />}
|
||||||
{results.value?.body && (
|
{results.value?.body && (
|
||||||
<div>
|
<div>
|
||||||
<TagFilter isClearable tags={query.tag} tagOptions={getTagOptions} onChange={onTagChange} />
|
<ActionRow
|
||||||
<br />
|
onLayoutChange={(v) => {
|
||||||
{query.datasource && (
|
if (v === SearchLayout.Folders) {
|
||||||
<Button
|
if (query.query) {
|
||||||
icon="times"
|
onQueryChange(''); // parent will clear the sort
|
||||||
variant="secondary"
|
}
|
||||||
onClick={() => onDatasourceChange(undefined)}
|
}
|
||||||
className={styles.clearClick}
|
onLayoutChange(v);
|
||||||
>
|
}}
|
||||||
Datasource: {query.datasource}
|
onSortChange={onSortChange}
|
||||||
</Button>
|
onTagFilterChange={onTagFilterChange}
|
||||||
)}
|
getTagOptions={getTagOptions}
|
||||||
<AutoSizer style={{ width: '100%', height: '2000px' }}>
|
onDatasourceChange={onDatasourceChange}
|
||||||
{({ width }) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table
|
<SearchResultsTable
|
||||||
data={results.value!.body}
|
data={results.value!.body}
|
||||||
|
showCheckbox={showManage}
|
||||||
|
layout={query.layout}
|
||||||
width={width}
|
width={width}
|
||||||
|
height={height}
|
||||||
tags={query.tag}
|
tags={query.tag}
|
||||||
onTagFilterChange={onTagChange}
|
onTagFilterChange={onTagChange}
|
||||||
onDatasourceChange={onDatasourceChange}
|
onDatasourceChange={onDatasourceChange}
|
||||||
@@ -109,10 +206,15 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
clearClick: css`
|
virtualizedGridItemWrapper: css`
|
||||||
&:hover {
|
padding: 4px;
|
||||||
text-decoration: line-through;
|
`,
|
||||||
|
wrapper: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> ul {
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
margin-bottom: 20px;
|
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { FC, FormEvent } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { HorizontalGroup, RadioButtonGroup, useStyles2, Checkbox, Button } from '@grafana/ui';
|
||||||
|
import { SortPicker } from 'app/core/components/Select/SortPicker';
|
||||||
|
import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||||
|
|
||||||
|
import { DashboardQuery, SearchLayout } from '../../types';
|
||||||
|
|
||||||
|
export const layoutOptions = [
|
||||||
|
{ value: SearchLayout.Folders, icon: 'folder', ariaLabel: 'View by folders' },
|
||||||
|
{ value: SearchLayout.List, icon: 'list-ul', ariaLabel: 'View as list' },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (config.featureToggles.dashboardPreviews) {
|
||||||
|
layoutOptions.push({ value: SearchLayout.Grid, icon: 'apps', ariaLabel: 'Grid view' });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onLayoutChange: (layout: SearchLayout) => void;
|
||||||
|
onSortChange: (value: SelectableValue) => void;
|
||||||
|
onStarredFilterChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
onTagFilterChange: (tags: string[]) => void;
|
||||||
|
getTagOptions: () => Promise<TermCount[]>;
|
||||||
|
onDatasourceChange: (ds?: string) => void;
|
||||||
|
query: DashboardQuery;
|
||||||
|
showStarredFilter?: boolean;
|
||||||
|
hideLayout?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidQueryLayout(q: DashboardQuery): SearchLayout {
|
||||||
|
// Folders is not valid when a query exists
|
||||||
|
if (q.layout === SearchLayout.Folders) {
|
||||||
|
if (q.query || q.sort) {
|
||||||
|
return SearchLayout.List;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return q.layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionRow: FC<Props> = ({
|
||||||
|
onLayoutChange,
|
||||||
|
onSortChange,
|
||||||
|
onStarredFilterChange = () => {},
|
||||||
|
onTagFilterChange,
|
||||||
|
getTagOptions,
|
||||||
|
onDatasourceChange,
|
||||||
|
query,
|
||||||
|
showStarredFilter,
|
||||||
|
hideLayout,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.actionRow}>
|
||||||
|
<div className={styles.rowContainer}>
|
||||||
|
<HorizontalGroup spacing="md" width="auto">
|
||||||
|
{!hideLayout && (
|
||||||
|
<RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={getValidQueryLayout(query)} />
|
||||||
|
)}
|
||||||
|
<SortPicker onChange={onSortChange} value={query.sort?.value} />
|
||||||
|
</HorizontalGroup>
|
||||||
|
</div>
|
||||||
|
<HorizontalGroup spacing="md" width="auto">
|
||||||
|
{showStarredFilter && (
|
||||||
|
<div className={styles.checkboxWrapper}>
|
||||||
|
<Checkbox label="Filter by starred" onChange={onStarredFilterChange} value={query.starred} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{query.datasource && (
|
||||||
|
<Button icon="times" variant="secondary" onClick={() => onDatasourceChange(undefined)}>
|
||||||
|
Datasource: {query.datasource}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<TagFilter isClearable tags={query.tag} tagOptions={getTagOptions} onChange={onTagFilterChange} />
|
||||||
|
</HorizontalGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ActionRow.displayName = 'ActionRow';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
actionRow: css`
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media only screen and (min-width: ${theme.v1.breakpoints.md}) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${theme.v1.spacing.lg} 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
rowContainer: css`
|
||||||
|
margin-right: ${theme.v1.spacing.md};
|
||||||
|
`,
|
||||||
|
checkboxWrapper: css`
|
||||||
|
label {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
+44
-12
@@ -9,12 +9,16 @@ import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
|
|||||||
import { getTableStyles } from '@grafana/ui/src/components/Table/styles';
|
import { getTableStyles } from '@grafana/ui/src/components/Table/styles';
|
||||||
|
|
||||||
import { LocationInfo } from '../../service';
|
import { LocationInfo } from '../../service';
|
||||||
|
import { SearchLayout } from '../../types';
|
||||||
|
|
||||||
import { generateColumns } from './columns';
|
import { generateColumns } from './columns';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
width: number;
|
width: number;
|
||||||
|
height: number;
|
||||||
|
showCheckbox: boolean;
|
||||||
|
layout: SearchLayout;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
onTagFilterChange: (tags: string[]) => void;
|
onTagFilterChange: (tags: string[]) => void;
|
||||||
onDatasourceChange: (datasource?: string) => void;
|
onDatasourceChange: (datasource?: string) => void;
|
||||||
@@ -25,6 +29,7 @@ export type TableColumn = Column & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface FieldAccess {
|
export interface FieldAccess {
|
||||||
|
uid: string; // the item UID
|
||||||
kind: string; // panel, dashboard, folder
|
kind: string; // panel, dashboard, folder
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -39,7 +44,18 @@ export interface FieldAccess {
|
|||||||
datasource: DataSourceRef[];
|
datasource: DataSourceRef[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange }: Props) => {
|
const skipHREF = new Set(['column-checkbox', 'column-datasource']);
|
||||||
|
|
||||||
|
export const SearchResultsTable = ({
|
||||||
|
data,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tags,
|
||||||
|
showCheckbox,
|
||||||
|
layout,
|
||||||
|
onTagFilterChange,
|
||||||
|
onDatasourceChange,
|
||||||
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const tableStyles = useStyles2(getTableStyles);
|
const tableStyles = useStyles2(getTableStyles);
|
||||||
|
|
||||||
@@ -56,9 +72,18 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange
|
|||||||
// React-table column definitions
|
// React-table column definitions
|
||||||
const access = useMemo(() => new DataFrameView<FieldAccess>(data), [data]);
|
const access = useMemo(() => new DataFrameView<FieldAccess>(data), [data]);
|
||||||
const memoizedColumns = useMemo(() => {
|
const memoizedColumns = useMemo(() => {
|
||||||
const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing;
|
const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing || layout === SearchLayout.Folders;
|
||||||
return generateColumns(access, isDashboardList, width, styles, tags, onTagFilterChange, onDatasourceChange);
|
return generateColumns(
|
||||||
}, [data.meta?.type, access, width, styles, tags, onTagFilterChange, onDatasourceChange]);
|
access,
|
||||||
|
isDashboardList,
|
||||||
|
width,
|
||||||
|
showCheckbox,
|
||||||
|
styles,
|
||||||
|
tags,
|
||||||
|
onTagFilterChange,
|
||||||
|
onDatasourceChange
|
||||||
|
);
|
||||||
|
}, [data.meta?.type, layout, access, width, styles, tags, showCheckbox, onTagFilterChange, onDatasourceChange]);
|
||||||
|
|
||||||
const options: TableOptions<{}> = useMemo(
|
const options: TableOptions<{}> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -80,15 +105,22 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange
|
|||||||
return (
|
return (
|
||||||
<div {...row.getRowProps({ style })} className={styles.rowContainer}>
|
<div {...row.getRowProps({ style })} className={styles.rowContainer}>
|
||||||
{row.cells.map((cell: Cell, index: number) => {
|
{row.cells.map((cell: Cell, index: number) => {
|
||||||
|
const body = (
|
||||||
|
<TableCell
|
||||||
|
key={index}
|
||||||
|
tableStyles={tableStyles}
|
||||||
|
cell={cell}
|
||||||
|
columnIndex={index}
|
||||||
|
columnCount={row.cells.length}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (skipHREF.has(cell.column.id)) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={url} key={index} className={styles.cellWrapper}>
|
<a href={url} key={index} className={styles.cellWrapper}>
|
||||||
<TableCell
|
{body}
|
||||||
key={index}
|
|
||||||
tableStyles={tableStyles}
|
|
||||||
cell={cell}
|
|
||||||
columnIndex={index}
|
|
||||||
columnCount={row.cells.length}
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -122,7 +154,7 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange
|
|||||||
<div {...getTableBodyProps()}>
|
<div {...getTableBodyProps()}>
|
||||||
{rows.length > 0 ? (
|
{rows.length > 0 ? (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
height={500}
|
height={height}
|
||||||
itemCount={rows.length}
|
itemCount={rows.length}
|
||||||
itemSize={tableStyles.rowHeight}
|
itemSize={tableStyles.rowHeight}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
+26
-12
@@ -8,42 +8,56 @@ import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell';
|
|||||||
|
|
||||||
import { LocationInfo } from '../../service';
|
import { LocationInfo } from '../../service';
|
||||||
|
|
||||||
import { FieldAccess, TableColumn } from './Table';
|
import { FieldAccess, TableColumn } from './SearchResultsTable';
|
||||||
|
|
||||||
export const generateColumns = (
|
export const generateColumns = (
|
||||||
data: DataFrameView<FieldAccess>,
|
data: DataFrameView<FieldAccess>,
|
||||||
isDashboardList: boolean,
|
isDashboardList: boolean,
|
||||||
availableWidth: number,
|
availableWidth: number,
|
||||||
|
showCheckbox: boolean,
|
||||||
styles: { [key: string]: string },
|
styles: { [key: string]: string },
|
||||||
tags: string[],
|
tags: string[],
|
||||||
onTagFilterChange: (tags: string[]) => void,
|
onTagFilterChange: (tags: string[]) => void,
|
||||||
onDatasourceChange: (datasource?: string) => void
|
onDatasourceChange: (datasource?: string) => void
|
||||||
): TableColumn[] => {
|
): TableColumn[] => {
|
||||||
const columns: TableColumn[] = [];
|
const columns: TableColumn[] = [];
|
||||||
const urlField = data.fields.url!;
|
const uidField = data.fields.uid!;
|
||||||
const access = data.fields;
|
const access = data.fields;
|
||||||
|
|
||||||
availableWidth -= 8; // ???
|
availableWidth -= 8; // ???
|
||||||
let width = 50;
|
let width = 50;
|
||||||
|
|
||||||
// TODO: Add optional checkbox support
|
// TODO: Add optional checkbox support
|
||||||
if (false) {
|
if (showCheckbox) {
|
||||||
// checkbox column
|
|
||||||
columns.push({
|
columns.push({
|
||||||
id: `column-checkbox`,
|
id: `column-checkbox`,
|
||||||
Header: () => (
|
Header: () => (
|
||||||
<div className={styles.checkboxHeader}>
|
<div className={styles.checkboxHeader}>
|
||||||
<Checkbox onChange={() => {}} />
|
<Checkbox
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('SELECT ALL!!!', e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Cell: () => (
|
|
||||||
<div className={styles.checkbox}>
|
|
||||||
<Checkbox onChange={() => {}} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
accessor: 'check',
|
|
||||||
field: urlField,
|
|
||||||
width: 30,
|
width: 30,
|
||||||
|
Cell: (p) => {
|
||||||
|
const uid = uidField.values.get(p.row.index);
|
||||||
|
return (
|
||||||
|
<div {...p.cellProps} className={p.cellStyle}>
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e) => {
|
||||||
|
console.log('SELECTED!!!', uid);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
field: uidField,
|
||||||
});
|
});
|
||||||
availableWidth -= width;
|
availableWidth -= width;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { DataFrame, DataSourceRef } from '@grafana/data';
|
|||||||
export interface QueryResult {
|
export interface QueryResult {
|
||||||
kind: string; // panel, dashboard, folder
|
kind: string; // panel, dashboard, folder
|
||||||
name: string;
|
name: string;
|
||||||
|
uid: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
url: string; // link to value (unique)
|
url: string; // link to value (unique)
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export type OnMoveItems = (selectedDashboards: DashboardSectionItem[], folder: F
|
|||||||
export enum SearchLayout {
|
export enum SearchLayout {
|
||||||
List = 'list',
|
List = 'list',
|
||||||
Folders = 'folders',
|
Folders = 'folders',
|
||||||
|
Grid = 'grid', // preview
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchQueryParams {
|
export interface SearchQueryParams {
|
||||||
|
|||||||
Reference in New Issue
Block a user