From 9b82e65b7ace12aa5e258976ec8a03a51cb6b31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Jan 2023 14:03:13 +0100 Subject: [PATCH] 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 --- .betterer.results | 6 +- .../components/PanelChrome/PanelContext.ts | 7 + .../grafana-ui/src/components/Table/types.ts | 6 +- packages/grafana-ui/src/components/index.ts | 7 +- .../core/components/OptionsUI/registry.tsx | 16 +- .../dashboard/dashgrid/PanelStateWrapper.tsx | 20 ++ public/app/features/explore/Explore.tsx | 14 +- .../explore/RawPrometheusContainer.tsx | 5 +- .../app/features/explore/TableContainer.tsx | 5 +- public/app/plugins/panel/table/TablePanel.tsx | 249 ++++++++---------- 10 files changed, 178 insertions(+), 157 deletions(-) diff --git a/.betterer.results b/.betterer.results index 63aa123eebb..7795b601770 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts index 509168f2ad3..309f3cc1b81 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts +++ b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts @@ -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 * diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 399466704da..4200c52907d 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -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; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index f5e93ebaaa8..154e11ad566 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index 817287f524d..99fb0ea55d1 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -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]; }; diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index b7e2a8daeda..bd84355854a 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -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 { 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 { ); } + 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; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 267a78fe8fe..7c5df5d230b 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { 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); diff --git a/public/app/features/explore/RawPrometheusContainer.tsx b/public/app/features/explore/RawPrometheusContainer.tsx index 905b6ca9405..c37a9859852 100644 --- a/public/app/features/explore/RawPrometheusContainer.tsx +++ b/public/app/features/explore/RawPrometheusContainer.tsx @@ -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; } diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index bdf6f6af4d7..affe1bb0a6e 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -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; } diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 6ec1ddbf42f..b86d0960c1b 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -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 {} -export class TablePanel extends Component { - 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 ; } - 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 = ( + 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 ( +
+ {tableElement} +
+
- ); } - 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 ; - } - - 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 ( -
- {this.renderTable(main, width, height - inputHeight - padding, subData)} -
-