mirror of https://github.com/grafana/grafana
Table: Adds adhoc filtering (#25467)
* Table: Adds adhoc filtering * Refactor: changes after PR comments * Refactor: hides filtering for data sources that do not support modifyQuery in Explore * Refactor: fixes strict null error * Changed tooltip position to above icon Co-authored-by: Torkel Ödegaard <torkel@grafana.com>pull/25511/head
parent
1040d824c5
commit
72b8300571
@ -0,0 +1,86 @@ |
||||
import React, { FC, useCallback, useState } from 'react'; |
||||
import { TableCellProps } from 'react-table'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
import { css, cx } from 'emotion'; |
||||
|
||||
import { stylesFactory, useTheme } from '../../themes'; |
||||
import { TableStyles } from './styles'; |
||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableFilterActionCallback } from './types'; |
||||
import { Icon, Tooltip } from '..'; |
||||
import { Props, renderCell } from './TableCell'; |
||||
|
||||
interface FilterableTableCellProps extends Pick<Props, 'cell' | 'field' | 'tableStyles'> { |
||||
onCellFilterAdded: TableFilterActionCallback; |
||||
cellProps: TableCellProps; |
||||
} |
||||
|
||||
export const FilterableTableCell: FC<FilterableTableCellProps> = ({ |
||||
cell, |
||||
field, |
||||
tableStyles, |
||||
onCellFilterAdded, |
||||
cellProps, |
||||
}) => { |
||||
const [showFilters, setShowFilter] = useState(false); |
||||
const onMouseOver = useCallback((event: React.MouseEvent<HTMLDivElement>) => setShowFilter(true), [setShowFilter]); |
||||
const onMouseLeave = useCallback((event: React.MouseEvent<HTMLDivElement>) => setShowFilter(false), [setShowFilter]); |
||||
const onFilterFor = useCallback( |
||||
(event: React.MouseEvent<HTMLDivElement>) => |
||||
onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: cell.value }), |
||||
[cell, field, onCellFilterAdded] |
||||
); |
||||
const onFilterOut = useCallback( |
||||
(event: React.MouseEvent<HTMLDivElement>) => |
||||
onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: cell.value }), |
||||
[cell, field, onCellFilterAdded] |
||||
); |
||||
const theme = useTheme(); |
||||
const styles = getFilterableTableCellStyles(theme, tableStyles); |
||||
|
||||
return ( |
||||
<div |
||||
{...cellProps} |
||||
className={showFilters ? styles.tableCellWrapper : tableStyles.tableCellWrapper} |
||||
onMouseOver={onMouseOver} |
||||
onMouseLeave={onMouseLeave} |
||||
> |
||||
{renderCell(cell, field, tableStyles)} |
||||
{showFilters && cell.value && ( |
||||
<div className={styles.filterWrapper}> |
||||
<div className={styles.filterItem}> |
||||
<Tooltip content="Filter for value" placement="top"> |
||||
<Icon name={'search-plus'} onClick={onFilterFor} /> |
||||
</Tooltip> |
||||
</div> |
||||
<div className={styles.filterItem}> |
||||
<Tooltip content="Filter out value" placement="top"> |
||||
<Icon name={'search-minus'} onClick={onFilterOut} /> |
||||
</Tooltip> |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const getFilterableTableCellStyles = stylesFactory((theme: GrafanaTheme, tableStyles: TableStyles) => ({ |
||||
tableCellWrapper: cx( |
||||
tableStyles.tableCellWrapper, |
||||
css` |
||||
display: inline-flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
` |
||||
), |
||||
filterWrapper: css` |
||||
label: filterWrapper; |
||||
display: inline-flex; |
||||
justify-content: space-around; |
||||
cursor: pointer; |
||||
`,
|
||||
filterItem: css` |
||||
label: filterItem; |
||||
color: ${theme.colors.textSemiWeak}; |
||||
padding: 0 ${theme.spacing.xxs}; |
||||
`,
|
||||
})); |
@ -1,38 +1,45 @@ |
||||
import React, { FC } from 'react'; |
||||
import { Cell } from 'react-table'; |
||||
import { Field } from '@grafana/data'; |
||||
|
||||
import { getTextAlign } from './utils'; |
||||
import { TableFilterActionCallback } from './types'; |
||||
import { TableStyles } from './styles'; |
||||
import { FilterableTableCell } from './FilterableTableCell'; |
||||
|
||||
interface Props { |
||||
export interface Props { |
||||
cell: Cell; |
||||
field: Field; |
||||
tableStyles: TableStyles; |
||||
onCellClick?: TableFilterActionCallback; |
||||
onCellFilterAdded?: TableFilterActionCallback; |
||||
} |
||||
|
||||
export const TableCell: FC<Props> = ({ cell, field, tableStyles, onCellClick }) => { |
||||
export const TableCell: FC<Props> = ({ cell, field, tableStyles, onCellFilterAdded }) => { |
||||
const filterable = field.config.filterable; |
||||
const cellProps = cell.getCellProps(); |
||||
|
||||
let onClick: ((event: React.SyntheticEvent) => void) | undefined = undefined; |
||||
|
||||
if (filterable && onCellClick) { |
||||
if (cellProps.style) { |
||||
cellProps.style.cursor = 'pointer'; |
||||
} |
||||
|
||||
onClick = () => onCellClick(cell.column.Header as string, cell.value); |
||||
} |
||||
|
||||
if (cellProps.style) { |
||||
cellProps.style.textAlign = getTextAlign(field); |
||||
} |
||||
|
||||
if (filterable && onCellFilterAdded) { |
||||
return ( |
||||
<FilterableTableCell |
||||
cell={cell} |
||||
field={field} |
||||
tableStyles={tableStyles} |
||||
onCellFilterAdded={onCellFilterAdded} |
||||
cellProps={cellProps} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div {...cellProps} onClick={onClick} className={tableStyles.tableCellWrapper}> |
||||
{cell.render('Cell', { field, tableStyles })} |
||||
<div {...cellProps} className={tableStyles.tableCellWrapper}> |
||||
{renderCell(cell, field, tableStyles)} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export const renderCell = (cell: Cell, field: Field, tableStyles: TableStyles) => |
||||
cell.render('Cell', { field, tableStyles }); |
||||
|
Loading…
Reference in new issue