mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
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};
|
||||||
|
`,
|
||||||
|
}));
|
@ -36,9 +36,9 @@ export interface Props {
|
|||||||
noHeader?: boolean;
|
noHeader?: boolean;
|
||||||
resizable?: boolean;
|
resizable?: boolean;
|
||||||
initialSortBy?: TableSortByFieldState[];
|
initialSortBy?: TableSortByFieldState[];
|
||||||
onCellClick?: TableFilterActionCallback;
|
|
||||||
onColumnResize?: TableColumnResizeActionCallback;
|
onColumnResize?: TableColumnResizeActionCallback;
|
||||||
onSortByChange?: TableSortByActionCallback;
|
onSortByChange?: TableSortByActionCallback;
|
||||||
|
onCellFilterAdded?: TableFilterActionCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}> {}
|
interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}> {}
|
||||||
@ -110,7 +110,15 @@ function getInitialState(props: Props, columns: Column[]): Partial<ReactTableInt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Table: FC<Props> = memo((props: Props) => {
|
export const Table: FC<Props> = memo((props: Props) => {
|
||||||
const { data, height, onCellClick, width, columnMinWidth = COLUMN_MIN_WIDTH, noHeader, resizable = true } = props;
|
const {
|
||||||
|
data,
|
||||||
|
height,
|
||||||
|
onCellFilterAdded,
|
||||||
|
width,
|
||||||
|
columnMinWidth = COLUMN_MIN_WIDTH,
|
||||||
|
noHeader,
|
||||||
|
resizable = true,
|
||||||
|
} = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const tableStyles = getTableStyles(theme);
|
const tableStyles = getTableStyles(theme);
|
||||||
|
|
||||||
@ -162,7 +170,7 @@ export const Table: FC<Props> = memo((props: Props) => {
|
|||||||
field={data.fields[index]}
|
field={data.fields[index]}
|
||||||
tableStyles={tableStyles}
|
tableStyles={tableStyles}
|
||||||
cell={cell}
|
cell={cell}
|
||||||
onCellClick={onCellClick}
|
onCellFilterAdded={onCellFilterAdded}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,38 +1,45 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Cell } from 'react-table';
|
import { Cell } from 'react-table';
|
||||||
import { Field } from '@grafana/data';
|
import { Field } from '@grafana/data';
|
||||||
|
|
||||||
import { getTextAlign } from './utils';
|
import { getTextAlign } from './utils';
|
||||||
import { TableFilterActionCallback } from './types';
|
import { TableFilterActionCallback } from './types';
|
||||||
import { TableStyles } from './styles';
|
import { TableStyles } from './styles';
|
||||||
|
import { FilterableTableCell } from './FilterableTableCell';
|
||||||
|
|
||||||
interface Props {
|
export interface Props {
|
||||||
cell: Cell;
|
cell: Cell;
|
||||||
field: Field;
|
field: Field;
|
||||||
tableStyles: TableStyles;
|
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 filterable = field.config.filterable;
|
||||||
const cellProps = cell.getCellProps();
|
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) {
|
if (cellProps.style) {
|
||||||
cellProps.style.textAlign = getTextAlign(field);
|
cellProps.style.textAlign = getTextAlign(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filterable && onCellFilterAdded) {
|
||||||
|
return (
|
||||||
|
<FilterableTableCell
|
||||||
|
cell={cell}
|
||||||
|
field={field}
|
||||||
|
tableStyles={tableStyles}
|
||||||
|
onCellFilterAdded={onCellFilterAdded}
|
||||||
|
cellProps={cellProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...cellProps} onClick={onClick} className={tableStyles.tableCellWrapper}>
|
<div {...cellProps} className={tableStyles.tableCellWrapper}>
|
||||||
{cell.render('Cell', { field, tableStyles })}
|
{renderCell(cell, field, tableStyles)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const renderCell = (cell: Cell, field: Field, tableStyles: TableStyles) =>
|
||||||
|
cell.render('Cell', { field, tableStyles });
|
||||||
|
@ -25,7 +25,11 @@ export interface TableRow {
|
|||||||
[x: string]: any;
|
[x: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TableFilterActionCallback = (key: string, value: string) => void;
|
export const FILTER_FOR_OPERATOR = '=';
|
||||||
|
export const FILTER_OUT_OPERATOR = '!=';
|
||||||
|
export type FilterOperator = typeof FILTER_FOR_OPERATOR | typeof FILTER_OUT_OPERATOR;
|
||||||
|
export type FilterItem = { key: string; value: string; operator: FilterOperator };
|
||||||
|
export type TableFilterActionCallback = (item: FilterItem) => void;
|
||||||
export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void;
|
export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void;
|
||||||
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
|
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ import { scanStopAction } from './state/actionTypes';
|
|||||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||||
import { TraceView } from './TraceView/TraceView';
|
import { TraceView } from './TraceView/TraceView';
|
||||||
import { SecondaryActions } from './SecondaryActions';
|
import { SecondaryActions } from './SecondaryActions';
|
||||||
|
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
@ -211,6 +212,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
this.props.setQueries(this.props.exploreId, [query]);
|
this.props.setQueries(this.props.exploreId, [query]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCellFilterAdded = (filter: FilterItem) => {
|
||||||
|
const { value, key, operator } = filter;
|
||||||
|
if (operator === FILTER_FOR_OPERATOR) {
|
||||||
|
this.onClickFilterLabel(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operator === FILTER_OUT_OPERATOR) {
|
||||||
|
this.onClickFilterOutLabel(key, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onClickFilterLabel = (key: string, value: string) => {
|
onClickFilterLabel = (key: string, value: string) => {
|
||||||
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
||||||
};
|
};
|
||||||
@ -366,7 +378,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mode === ExploreMode.Metrics && (
|
{mode === ExploreMode.Metrics && (
|
||||||
<TableContainer width={width} exploreId={exploreId} onClickCell={this.onClickFilterLabel} />
|
<TableContainer
|
||||||
|
width={width}
|
||||||
|
exploreId={exploreId}
|
||||||
|
onCellFilterAdded={
|
||||||
|
this.props.datasourceInstance?.modifyQuery ? this.onCellFilterAdded : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{mode === ExploreMode.Logs && (
|
{mode === ExploreMode.Logs && (
|
||||||
<LogsContainer
|
<LogsContainer
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow, render } from 'enzyme';
|
import { render, shallow } from 'enzyme';
|
||||||
import { TableContainer } from './TableContainer';
|
import { TableContainer } from './TableContainer';
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame } from '@grafana/data';
|
||||||
import { toggleTable } from './state/actions';
|
import { toggleTable } from './state/actions';
|
||||||
@ -11,7 +11,7 @@ describe('TableContainer', () => {
|
|||||||
exploreId: ExploreId.left as ExploreId,
|
exploreId: ExploreId.left as ExploreId,
|
||||||
loading: false,
|
loading: false,
|
||||||
width: 800,
|
width: 800,
|
||||||
onClickCell: jest.fn(),
|
onCellFilterAdded: jest.fn(),
|
||||||
showingTable: true,
|
showingTable: true,
|
||||||
tableResult: {} as DataFrame,
|
tableResult: {} as DataFrame,
|
||||||
toggleTable: {} as typeof toggleTable,
|
toggleTable: {} as typeof toggleTable,
|
||||||
@ -26,7 +26,7 @@ describe('TableContainer', () => {
|
|||||||
exploreId: ExploreId.left as ExploreId,
|
exploreId: ExploreId.left as ExploreId,
|
||||||
loading: false,
|
loading: false,
|
||||||
width: 800,
|
width: 800,
|
||||||
onClickCell: jest.fn(),
|
onCellFilterAdded: jest.fn(),
|
||||||
showingTable: true,
|
showingTable: true,
|
||||||
tableResult: {
|
tableResult: {
|
||||||
name: 'TableResultName',
|
name: 'TableResultName',
|
||||||
|
@ -2,19 +2,20 @@ import React, { PureComponent } from 'react';
|
|||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame } from '@grafana/data';
|
||||||
import { Table, Collapse } from '@grafana/ui';
|
import { Collapse, Table } from '@grafana/ui';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { toggleTable } from './state/actions';
|
import { toggleTable } from './state/actions';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
import { MetaInfoText } from './MetaInfoText';
|
import { MetaInfoText } from './MetaInfoText';
|
||||||
|
import { FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||||
|
|
||||||
interface TableContainerProps {
|
interface TableContainerProps {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
onClickCell: (key: string, value: string) => void;
|
onCellFilterAdded?: (filter: FilterItem) => void;
|
||||||
showingTable: boolean;
|
showingTable: boolean;
|
||||||
tableResult?: DataFrame;
|
tableResult?: DataFrame;
|
||||||
toggleTable: typeof toggleTable;
|
toggleTable: typeof toggleTable;
|
||||||
@ -37,7 +38,7 @@ export class TableContainer extends PureComponent<TableContainerProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, onClickCell, showingTable, tableResult, width } = this.props;
|
const { loading, onCellFilterAdded, showingTable, tableResult, width } = this.props;
|
||||||
|
|
||||||
const height = this.getTableHeight();
|
const height = this.getTableHeight();
|
||||||
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
||||||
@ -46,7 +47,7 @@ export class TableContainer extends PureComponent<TableContainerProps> {
|
|||||||
return (
|
return (
|
||||||
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
|
<Collapse label="Table" loading={loading} collapsible isOpen={showingTable} onToggle={this.onClickTableButton}>
|
||||||
{hasTableResult ? (
|
{hasTableResult ? (
|
||||||
<Table data={tableResult!} width={tableWidth} height={height} onCellClick={onClickCell} />
|
<Table data={tableResult!} width={tableWidth} height={height} onCellFilterAdded={onCellFilterAdded} />
|
||||||
) : (
|
) : (
|
||||||
<MetaInfoText metaItems={[{ value: '0 series returned' }]} />
|
<MetaInfoText metaItems={[{ value: '0 series returned' }]} />
|
||||||
)}
|
)}
|
||||||
|
@ -20,6 +20,7 @@ import { PromOptions, PromQuery } from './types';
|
|||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { VariableHide } from '../../../features/variables/types';
|
import { VariableHide } from '../../../features/variables/types';
|
||||||
|
import { describe } from '../../../../test/lib/common';
|
||||||
|
|
||||||
const datasourceRequestMock = jest.fn().mockResolvedValue(createDefaultPromResponse());
|
const datasourceRequestMock = jest.fn().mockResolvedValue(createDefaultPromResponse());
|
||||||
|
|
||||||
@ -1886,6 +1887,68 @@ describe('prepareTargets', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('modifyQuery', () => {
|
||||||
|
describe('when called with ADD_FILTER', () => {
|
||||||
|
describe('and query has no labels', () => {
|
||||||
|
it('then the correct label should be added', () => {
|
||||||
|
const query: PromQuery = { refId: 'A', expr: 'go_goroutines' };
|
||||||
|
const action = { key: 'cluster', value: 'us-cluster', type: 'ADD_FILTER' };
|
||||||
|
const instanceSettings = ({ jsonData: {} } as unknown) as DataSourceInstanceSettings<PromOptions>;
|
||||||
|
const ds = new PrometheusDatasource(instanceSettings);
|
||||||
|
|
||||||
|
const result = ds.modifyQuery(query, action);
|
||||||
|
|
||||||
|
expect(result.refId).toEqual('A');
|
||||||
|
expect(result.expr).toEqual('go_goroutines{cluster="us-cluster"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and query has labels', () => {
|
||||||
|
it('then the correct label should be added', () => {
|
||||||
|
const query: PromQuery = { refId: 'A', expr: 'go_goroutines{cluster="us-cluster"}' };
|
||||||
|
const action = { key: 'pod', value: 'pod-123', type: 'ADD_FILTER' };
|
||||||
|
const instanceSettings = ({ jsonData: {} } as unknown) as DataSourceInstanceSettings<PromOptions>;
|
||||||
|
const ds = new PrometheusDatasource(instanceSettings);
|
||||||
|
|
||||||
|
const result = ds.modifyQuery(query, action);
|
||||||
|
|
||||||
|
expect(result.refId).toEqual('A');
|
||||||
|
expect(result.expr).toEqual('go_goroutines{cluster="us-cluster",pod="pod-123"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with ADD_FILTER_OUT', () => {
|
||||||
|
describe('and query has no labels', () => {
|
||||||
|
it('then the correct label should be added', () => {
|
||||||
|
const query: PromQuery = { refId: 'A', expr: 'go_goroutines' };
|
||||||
|
const action = { key: 'cluster', value: 'us-cluster', type: 'ADD_FILTER_OUT' };
|
||||||
|
const instanceSettings = ({ jsonData: {} } as unknown) as DataSourceInstanceSettings<PromOptions>;
|
||||||
|
const ds = new PrometheusDatasource(instanceSettings);
|
||||||
|
|
||||||
|
const result = ds.modifyQuery(query, action);
|
||||||
|
|
||||||
|
expect(result.refId).toEqual('A');
|
||||||
|
expect(result.expr).toEqual('go_goroutines{cluster!="us-cluster"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and query has labels', () => {
|
||||||
|
it('then the correct label should be added', () => {
|
||||||
|
const query: PromQuery = { refId: 'A', expr: 'go_goroutines{cluster="us-cluster"}' };
|
||||||
|
const action = { key: 'pod', value: 'pod-123', type: 'ADD_FILTER_OUT' };
|
||||||
|
const instanceSettings = ({ jsonData: {} } as unknown) as DataSourceInstanceSettings<PromOptions>;
|
||||||
|
const ds = new PrometheusDatasource(instanceSettings);
|
||||||
|
|
||||||
|
const result = ds.modifyQuery(query, action);
|
||||||
|
|
||||||
|
expect(result.refId).toEqual('A');
|
||||||
|
expect(result.expr).toEqual('go_goroutines{cluster="us-cluster",pod!="pod-123"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function createDataRequest(targets: any[], overrides?: Partial<DataQueryRequest>): DataQueryRequest<PromQuery> {
|
function createDataRequest(targets: any[], overrides?: Partial<DataQueryRequest>): DataQueryRequest<PromQuery> {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
app: CoreApp.Dashboard,
|
app: CoreApp.Dashboard,
|
||||||
|
@ -693,6 +693,10 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
expression = addLabelToQuery(expression, action.key, action.value);
|
expression = addLabelToQuery(expression, action.key, action.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'ADD_FILTER_OUT': {
|
||||||
|
expression = addLabelToQuery(expression, action.key, action.value, '!=');
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'ADD_HISTOGRAM_QUANTILE': {
|
case 'ADD_HISTOGRAM_QUANTILE': {
|
||||||
expression = `histogram_quantile(0.95, sum(rate(${expression}[5m])) by (le))`;
|
expression = `histogram_quantile(0.95, sum(rate(${expression}[5m])) by (le))`;
|
||||||
break;
|
break;
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Table, Select } from '@grafana/ui';
|
import { Select, Table } from '@grafana/ui';
|
||||||
import { FieldMatcherID, PanelProps, DataFrame, SelectableValue, getFrameDisplayName } from '@grafana/data';
|
import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
|
import { FilterItem, TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
|
||||||
|
import { dispatch } from '../../../store/store';
|
||||||
|
import { applyFilterFromTable } from '../../../features/variables/adhoc/actions';
|
||||||
|
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||||
|
|
||||||
interface Props extends PanelProps<Options> {}
|
interface Props extends PanelProps<Options> {}
|
||||||
|
|
||||||
@ -62,6 +65,20 @@ export class TablePanel extends Component<Props> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCellFilterAdded = (filter: FilterItem) => {
|
||||||
|
const { key, value, operator } = filter;
|
||||||
|
const panelModel = getDashboardSrv()
|
||||||
|
.getCurrent()
|
||||||
|
.getPanelById(this.props.id);
|
||||||
|
const datasource = panelModel?.datasource;
|
||||||
|
|
||||||
|
if (!datasource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(applyFilterFromTable({ datasource, key, operator, value }));
|
||||||
|
};
|
||||||
|
|
||||||
renderTable(frame: DataFrame, width: number, height: number) {
|
renderTable(frame: DataFrame, width: number, height: number) {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
|
|
||||||
@ -75,6 +92,7 @@ export class TablePanel extends Component<Props> {
|
|||||||
initialSortBy={options.sortBy}
|
initialSortBy={options.sortBy}
|
||||||
onSortByChange={this.onSortByChange}
|
onSortByChange={this.onSortByChange}
|
||||||
onColumnResize={this.onColumnResize}
|
onColumnResize={this.onColumnResize}
|
||||||
|
onCellFilterAdded={this.onCellFilterAdded}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user