mirror of
https://github.com/grafana/grafana.git
synced 2025-01-07 22:53:56 -06:00
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:
parent
29f8722c91
commit
9b82e65b7a
@ -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"],
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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];
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user