TablePanel: Refactor to functional component and move add ad hoc filter action to PanelContext (#61360)

* FieldOptions: Add filterable as registry added field item

* Refactor to functional component, move ad hoc filter to PanelStateWrapper

* review tweaks
This commit is contained in:
Torkel Ödegaard 2023-01-19 14:03:13 +01:00 committed by GitHub
parent 29f8722c91
commit 9b82e65b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 178 additions and 157 deletions

View File

@ -2436,7 +2436,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "79"],
[0, 0, 0, "Unexpected any. Specify a different type.", "80"],
[0, 0, 0, "Do not use any type assertions.", "81"],
[0, 0, 0, "Unexpected any. Specify a different type.", "82"]
[0, 0, 0, "Unexpected any. Specify a different type.", "82"],
[0, 0, 0, "Do not use any type assertions.", "83"],
[0, 0, 0, "Unexpected any. Specify a different type.", "84"],
[0, 0, 0, "Do not use any type assertions.", "85"],
[0, 0, 0, "Unexpected any. Specify a different type.", "86"]
],
"public/app/core/components/OptionsUI/string.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -10,6 +10,8 @@ import {
CoreApp,
} from '@grafana/data';
import { AdHocFilterItem } from '../Table/types';
import { SeriesVisibilityChangeMode } from '.';
/** @alpha */
@ -38,6 +40,11 @@ export interface PanelContext {
onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void;
onAnnotationDelete?: (id: string) => void;
/**
* Used from visualizations like Table to add ad-hoc filters from cell values
*/
onAddAdHocFilter?: (item: AdHocFilterItem) => void;
/**
* Enables modifying thresholds directly from the panel
*

View File

@ -19,9 +19,9 @@ export interface TableRow {
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 AdHocFilterOperator = typeof FILTER_FOR_OPERATOR | typeof FILTER_OUT_OPERATOR;
export type AdHocFilterItem = { key: string; value: string; operator: AdHocFilterOperator };
export type TableFilterActionCallback = (item: AdHocFilterItem) => void;
export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void;
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;

View File

@ -80,7 +80,12 @@ export { PageToolbar } from './PageLayout/PageToolbar';
export { SetInterval } from './SetInterval/SetInterval';
export { Table } from './Table/Table';
export { TableCellDisplayMode, type TableSortByFieldState, type TableFooterCalc } from './Table/types';
export {
TableCellDisplayMode,
type TableSortByFieldState,
type TableFooterCalc,
type AdHocFilterItem,
} from './Table/types';
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
export { TabsBar } from './Tabs/TabsBar';
export { Tab } from './Tabs/Tab';

View File

@ -25,6 +25,7 @@ import {
StatsPickerConfigSettings,
displayNameOverrideProcessor,
FieldNamePickerConfigSettings,
booleanOverrideProcessor,
} from '@grafana/data';
import { RadioButtonGroup, TimeZonePicker, Switch } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
@ -381,5 +382,18 @@ export const getAllStandardFieldConfigs = () => {
getItemsCount: (value) => (value ? value.steps.length : 0),
};
return [unit, min, max, decimals, displayName, color, noValue, links, mappings, thresholds];
const filterable: FieldConfigPropertyItem<{}, boolean | undefined, {}> = {
id: 'filterable',
path: 'filterable',
name: 'Ad-hoc filterable',
hideFromDefaults: true,
editor: standardEditorsRegistry.get('boolean').editor as any,
override: standardEditorsRegistry.get('boolean').editor as any,
process: booleanOverrideProcessor,
shouldApply: () => true,
settings: {},
category,
};
return [unit, min, max, decimals, displayName, color, noValue, links, mappings, thresholds, filterable];
};

View File

@ -10,6 +10,7 @@ import {
DashboardCursorSync,
EventFilterOptions,
FieldConfigSource,
getDataSourceRef,
getDefaultTimeRange,
LinkModel,
LoadingState,
@ -32,13 +33,17 @@ import {
PanelContextProvider,
PanelPadding,
SeriesVisibilityChangeMode,
AdHocFilterItem,
} from '@grafana/ui';
import { PANEL_BORDER } from 'app/core/constants';
import { profiler } from 'app/core/profiler';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { InspectTab } from 'app/features/inspector/types';
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { applyFilterFromTable } from 'app/features/variables/adhoc/actions';
import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory';
import { dispatch } from 'app/store/store';
import { RenderEvent } from 'app/types/events';
import { isSoloRoute } from '../../../routes/utils';
@ -108,6 +113,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
canAddAnnotations: props.dashboard.canAddAnnotations.bind(props.dashboard),
canEditAnnotations: props.dashboard.canEditAnnotations.bind(props.dashboard),
canDeleteAnnotations: props.dashboard.canDeleteAnnotations.bind(props.dashboard),
onAddAdHocFilter: this.onAddAdHocFilter,
},
data: this.getInitialPanelDataState(),
};
@ -444,6 +450,20 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
);
}
onAddAdHocFilter = (filter: AdHocFilterItem) => {
const { key, value, operator } = filter;
// When the datasource is null/undefined (for a default datasource), we use getInstanceSettings
// to find the real datasource ref for the default datasource.
const datasourceInstance = getDatasourceSrv().getInstanceSettings(this.props.panel.datasource);
const datasourceRef = datasourceInstance && getDataSourceRef(datasourceInstance);
if (!datasourceRef) {
return;
}
dispatch(applyFilterFromTable({ datasource: datasourceRef, key, operator, value }));
};
renderPanelContent(innerWidth: number, innerHeight: number) {
const { panel, plugin, dashboard } = this.props;
const { renderCounter, data } = this.state;

View File

@ -18,8 +18,16 @@ import {
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2, PanelContainer, Alert } from '@grafana/ui';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
import {
CustomScrollbar,
ErrorBoundaryAlert,
Themeable2,
withTheme2,
PanelContainer,
Alert,
AdHocFilterItem,
} from '@grafana/ui';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types';
import appEvents from 'app/core/app_events';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider';
@ -157,7 +165,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
this.props.setQueries(this.props.exploreId, [query]);
};
onCellFilterAdded = (filter: FilterItem) => {
onCellFilterAdded = (filter: AdHocFilterItem) => {
const { value, key, operator } = filter;
if (operator === FILTER_FOR_OPERATOR) {
this.onClickFilterLabel(key, value);

View File

@ -4,8 +4,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { applyFieldOverrides, DataFrame, SelectableValue, SplitOpen, TimeZone, ValueLinkConfig } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime/src';
import { Collapse, RadioButtonGroup, Table } from '@grafana/ui';
import { FilterItem } from '@grafana/ui/src/components/Table/types';
import { Collapse, RadioButtonGroup, Table, AdHocFilterItem } from '@grafana/ui';
import { config } from 'app/core/config';
import { PANEL_BORDER } from 'app/core/constants';
import { StoreState, TABLE_RESULTS_STYLE } from 'app/types';
@ -20,7 +19,7 @@ interface RawPrometheusContainerProps {
exploreId: ExploreId;
width: number;
timeZone: TimeZone;
onCellFilterAdded?: (filter: FilterItem) => void;
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
showRawPrometheus?: boolean;
splitOpenFn: SplitOpen;
}

View File

@ -2,8 +2,7 @@ import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { ValueLinkConfig, applyFieldOverrides, TimeZone, SplitOpen, DataFrame } from '@grafana/data';
import { Collapse, Table } from '@grafana/ui';
import { FilterItem } from '@grafana/ui/src/components/Table/types';
import { Collapse, Table, AdHocFilterItem } from '@grafana/ui';
import { config } from 'app/core/config';
import { PANEL_BORDER } from 'app/core/constants';
import { StoreState } from 'app/types';
@ -17,7 +16,7 @@ interface TableContainerProps {
exploreId: ExploreId;
width: number;
timeZone: TimeZone;
onCellFilterAdded?: (filter: FilterItem) => void;
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
splitOpenFn: SplitOpen;
}

View File

@ -1,164 +1,129 @@
import { css } from '@emotion/css';
import React, { Component } from 'react';
import React from 'react';
import {
DataFrame,
FieldMatcherID,
getDataSourceRef,
getFrameDisplayName,
PanelProps,
SelectableValue,
} from '@grafana/data';
import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { Select, Table } from '@grafana/ui';
import { FilterItem, TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
import { config } from 'app/core/config';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
import { applyFilterFromTable } from '../../../features/variables/adhoc/actions';
import { dispatch } from '../../../store/store';
import { Select, Table, usePanelContext, useTheme2 } from '@grafana/ui';
import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
import { PanelOptions } from './models.gen';
interface Props extends PanelProps<PanelOptions> {}
export class TablePanel extends Component<Props> {
constructor(props: Props) {
super(props);
export function TablePanel(props: Props) {
const { data, height, width, options, fieldConfig, id } = props;
const theme = useTheme2();
const panelContext = usePanelContext();
const frames = data.series;
const mainFrames = frames.filter((f) => f.meta?.custom?.parentRowIndex === undefined);
const subFrames = frames.filter((f) => f.meta?.custom?.parentRowIndex !== undefined);
const count = mainFrames?.length;
const hasFields = mainFrames[0]?.fields.length;
const currentIndex = getCurrentFrameIndex(mainFrames, options);
const main = mainFrames[currentIndex];
let tableHeight = height;
let subData = subFrames;
if (!count || !hasFields) {
return <PanelDataErrorView panelId={id} fieldConfig={fieldConfig} data={data} />;
}
onColumnResize = (fieldDisplayName: string, width: number) => {
const { fieldConfig } = this.props;
const { overrides } = fieldConfig;
if (count > 1) {
const inputHeight = theme.spacing.gridSize * theme.components.height.md;
const padding = theme.spacing.gridSize;
const matcherId = FieldMatcherID.byName;
const propId = 'custom.width';
tableHeight = height - inputHeight - padding;
subData = subFrames.filter((f) => f.refId === main.refId);
}
// look for existing override
const override = overrides.find((o) => o.matcher.id === matcherId && o.matcher.options === fieldDisplayName);
const tableElement = (
<Table
height={tableHeight}
width={width}
data={main}
noHeader={!options.showHeader}
showTypeIcons={options.showTypeIcons}
resizable={true}
initialSortBy={options.sortBy}
onSortByChange={(sortBy) => onSortByChange(sortBy, props)}
onColumnResize={(displayName, width) => onColumnResize(displayName, width, props)}
onCellFilterAdded={panelContext.onAddAdHocFilter}
footerOptions={options.footer}
enablePagination={options.footer?.enablePagination}
subData={subData}
/>
);
if (override) {
// look for existing property
const property = override.properties.find((prop) => prop.id === propId);
if (property) {
property.value = width;
} else {
override.properties.push({ id: propId, value: width });
}
if (count === 1) {
return tableElement;
}
const names = mainFrames.map((frame, index) => {
return {
label: getFrameDisplayName(frame),
value: index,
};
});
return (
<div className={tableStyles.wrapper}>
{tableElement}
<div className={tableStyles.selectWrapper}>
<Select options={names} value={names[currentIndex]} onChange={(val) => onChangeTableSelection(val, props)} />
</div>
</div>
);
}
function getCurrentFrameIndex(frames: DataFrame[], options: PanelOptions) {
return options.frameIndex > 0 && options.frameIndex < frames.length ? options.frameIndex : 0;
}
function onColumnResize(fieldDisplayName: string, width: number, props: Props) {
const { fieldConfig } = props;
const { overrides } = fieldConfig;
const matcherId = FieldMatcherID.byName;
const propId = 'custom.width';
// look for existing override
const override = overrides.find((o) => o.matcher.id === matcherId && o.matcher.options === fieldDisplayName);
if (override) {
// look for existing property
const property = override.properties.find((prop) => prop.id === propId);
if (property) {
property.value = width;
} else {
overrides.push({
matcher: { id: matcherId, options: fieldDisplayName },
properties: [{ id: propId, value: width }],
});
override.properties.push({ id: propId, value: width });
}
this.props.onFieldConfigChange({
...fieldConfig,
overrides,
} else {
overrides.push({
matcher: { id: matcherId, options: fieldDisplayName },
properties: [{ id: propId, value: width }],
});
};
onSortByChange = (sortBy: TableSortByFieldState[]) => {
this.props.onOptionsChange({
...this.props.options,
sortBy,
});
};
onChangeTableSelection = (val: SelectableValue<number>) => {
this.props.onOptionsChange({
...this.props.options,
frameIndex: val.value || 0,
});
// Force a redraw -- but no need to re-query
this.forceUpdate();
};
onCellFilterAdded = (filter: FilterItem) => {
const { key, value, operator } = filter;
const panelModel = getDashboardSrv().getCurrent()?.getPanelById(this.props.id);
if (!panelModel) {
return;
}
// When the datasource is null/undefined (for a default datasource), we use getInstanceSettings
// to find the real datasource ref for the default datasource.
const datasourceInstance = getDatasourceSrv().getInstanceSettings(panelModel.datasource);
const datasourceRef = datasourceInstance && getDataSourceRef(datasourceInstance);
if (!datasourceRef) {
return;
}
dispatch(applyFilterFromTable({ datasource: datasourceRef, key, operator, value }));
};
renderTable(frame: DataFrame, width: number, height: number, subData?: DataFrame[]) {
const { options } = this.props;
return (
<Table
height={height}
width={width}
data={frame}
noHeader={!options.showHeader}
showTypeIcons={options.showTypeIcons}
resizable={true}
initialSortBy={options.sortBy}
onSortByChange={this.onSortByChange}
onColumnResize={this.onColumnResize}
onCellFilterAdded={this.onCellFilterAdded}
footerOptions={options.footer}
enablePagination={options.footer?.enablePagination}
subData={subData}
/>
);
}
getCurrentFrameIndex(frames: DataFrame[], options: PanelOptions) {
return options.frameIndex > 0 && options.frameIndex < frames.length ? options.frameIndex : 0;
}
props.onFieldConfigChange({
...fieldConfig,
overrides,
});
}
render() {
const { data, height, width, options, fieldConfig, id } = this.props;
function onSortByChange(sortBy: TableSortByFieldState[], props: Props) {
props.onOptionsChange({
...props.options,
sortBy,
});
}
const frames = data.series;
const mainFrames = frames.filter((f) => f.meta?.custom?.parentRowIndex === undefined);
const subFrames = frames.filter((f) => f.meta?.custom?.parentRowIndex !== undefined);
const count = mainFrames?.length;
const hasFields = mainFrames[0]?.fields.length;
if (!count || !hasFields) {
return <PanelDataErrorView panelId={id} fieldConfig={fieldConfig} data={data} />;
}
if (count > 1) {
const inputHeight = config.theme2.spacing.gridSize * config.theme2.components.height.md;
const padding = 8 * 2;
const currentIndex = this.getCurrentFrameIndex(mainFrames, options);
const names = mainFrames.map((frame, index) => {
return {
label: getFrameDisplayName(frame),
value: index,
};
});
const main = mainFrames[currentIndex];
const subData = subFrames.filter((f) => f.refId === main.refId);
return (
<div className={tableStyles.wrapper}>
{this.renderTable(main, width, height - inputHeight - padding, subData)}
<div className={tableStyles.selectWrapper}>
<Select options={names} value={names[currentIndex]} onChange={this.onChangeTableSelection} />
</div>
</div>
);
}
const subData = frames.filter((f) => f.meta?.custom?.parentRowIndex !== undefined);
return this.renderTable(data.series[0], width, height, subData);
}
function onChangeTableSelection(val: SelectableValue<number>, props: Props) {
props.onOptionsChange({
...props.options,
frameIndex: val.value || 0,
});
}
const tableStyles = {
@ -176,6 +141,6 @@ const tableStyles = {
height: 100%;
`,
selectWrapper: css`
padding: 8px;
padding: 8px 8px 0px 8px;
`,
};