Commit f23ecc40 by Torkel Ödegaard Committed by GitHub

Inspect: Allow showing data without transformations and field config is applied (#24314)

* Inspect: Should not subscribe to transformed data

* PQR- allow controll whether or not field overrides and transformations should be applied

* UI for inspector data options

* fix

* Null check fix

* Update public/app/features/dashboard/components/Inspector/InspectDataTab.tsx

* Update public/app/features/dashboard/components/Inspector/InspectDataTab.tsx

* Apply transformations by default

* Update panel inspect docs

* Fix apply overrides

* Apply time formatting in panel inspect

* fix ts

* Post review update

* Update docs/sources/panels/inspect-panel.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* lazy numbering

* fix ts

* Renames

* Renames 2

* Layout update

* Run shared request without field config

* Minor details

* fix ts

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
parent 9e24c094
......@@ -19,7 +19,7 @@ The panel inspector displays Inspect: <NameOfPanelBeingInspected> at the top of
The panel inspector consists of four tabs:
* **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings are not applied.
* **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings are not applied by default.
* **Stats tab -** Shows how long your query takes and how much it returns.
* **JSON tab -** Allows you to view and copy the panel JSON, panel data JSON, and data frame structure JSON. This is useful if you are provisioning or administering Grafana.
* **Query tab -** Shows you the requests to the server sent when Grafana queries the data source.
......@@ -42,14 +42,18 @@ The panel inspector pane opens on the right side of the screen.
### Inspect raw query results
View raw query results in a table. This is the data returned by the query with transformations applied and before the panel applies field configurations or overrides.
View raw query results in a table. This is the data returned by the query with transformations applied and before the panel applies field options or field option overrides.
1. Open the panel inspector and then click the **Data** tab or in the panel menu click **Inspect > Data**.
2. If your panel contains multiple queries or queries multiple nodes, then you have additional options.
1. If your panel contains multiple queries or queries multiple nodes, then you have additional options.
* **Select result -** Choose which result set data you want to view.
* **Transform data**
* **Join by time -** View raw data from all your queries at once, one result set per column. Click a column heading to reorder the data.
View raw query results in a table with field options and options overrides applied:
1. Open the **Data** tab in panel inspector.
1. Click on **Data display options** above the table.
1. Click on **Apply field configuration** toggle button.
### Download raw query results as CSV
......
......@@ -136,8 +136,8 @@ const getStyles = stylesFactory(
align-items: ${align};
&:last-child {
margin-bottom: 0;
margin-right: 0;
margin-bottom: ${orientation === Orientation.Vertical && 0};
margin-right: ${orientation === Orientation.Horizontal && 0};
}
`,
};
......
......@@ -7,18 +7,14 @@ export const DefaultCell: FC<TableCellProps> = props => {
const { field, cell, tableStyles, row } = props;
let link: LinkModel<any> | undefined;
if (!field.display) {
return null;
}
const displayValue = field.display(cell.value);
const displayValue = field.display ? field.display(cell.value) : cell.value;
if (field.getLinks) {
link = field.getLinks({
valueRowIndex: row.index,
})[0];
}
const value = formattedValueToString(displayValue);
const value = field.display ? formattedValueToString(displayValue) : displayValue;
return (
<div className={tableStyles.tableCell}>
......
......@@ -8,18 +8,27 @@ import {
transformDataFrame,
getFrameDisplayName,
} from '@grafana/data';
import { Button, Field, Icon, Select, Table } from '@grafana/ui';
import { Button, Field, Icon, LegacyForms, Select, Table } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import AutoSizer from 'react-virtualized-auto-sizer';
import { getPanelInspectorStyles } from './styles';
import { config } from 'app/core/config';
import { saveAs } from 'file-saver';
import { cx } from 'emotion';
import { css, cx } from 'emotion';
import { GetDataOptions } from '../../state/PanelQueryRunner';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
const { Switch } = LegacyForms;
interface Props {
dashboard: DashboardModel;
panel: PanelModel;
data: DataFrame[];
isLoading: boolean;
options: GetDataOptions;
onOptionsChange: (options: GetDataOptions) => void;
}
interface State {
......@@ -55,6 +64,10 @@ export class InspectDataTab extends PureComponent<Props, State> {
onTransformationChange = (value: SelectableValue<DataTransformerID>) => {
this.setState({ transformId: value.value, dataFrameIndex: 0 });
this.props.onOptionsChange({
...this.props.options,
withTransforms: false,
});
};
getTransformedData(): DataFrame[] {
......@@ -74,10 +87,19 @@ export class InspectDataTab extends PureComponent<Props, State> {
}
getProcessedData(): DataFrame[] {
const { options } = this.props;
let data = this.props.data;
if (this.state.transformId !== DataTransformerID.noop) {
data = this.getTransformedData();
}
// We need to apply field config even though it was already applied in the PanelQueryRunner.
// That's because transformers create new fields and data frames, so i.e. display processor is no longer there
return applyFieldOverrides({
data: this.getTransformedData(),
data,
theme: config.theme,
fieldConfig: { defaults: {}, overrides: [] },
fieldConfig: options.withFieldConfig ? this.props.panel.fieldConfig : { defaults: {}, overrides: [] },
replaceVariables: (value: string) => {
return value;
},
......@@ -85,7 +107,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
}
render() {
const { isLoading, data } = this.props;
const { isLoading, data, options, onOptionsChange } = this.props;
const { dataFrameIndex, transformId, transformationOptions } = this.state;
const styles = getPanelInspectorStyles();
......@@ -110,25 +132,73 @@ export class InspectDataTab extends PureComponent<Props, State> {
};
});
const panelTransformations = this.props.panel.getTransformations();
return (
<div className={styles.dataTabContent} aria-label={selectors.components.PanelInspector.Data.content}>
<div className={styles.toolbar}>
{data.length > 1 && (
<Field label="Transform data" className="flex-grow-1">
<Select options={transformationOptions} value={transformId} onChange={this.onTransformationChange} />
</Field>
)}
{choices.length > 1 && (
<Field label="Select result" className={cx(styles.toolbarItem, 'flex-grow-1')}>
<Select options={choices} value={dataFrameIndex} onChange={this.onSelectedFrameChanged} />
</Field>
)}
<div className={styles.downloadCsv}>
<div className={styles.actionsWrapper}>
<div className={styles.leftActions}>
<div className={styles.selects}>
{data.length > 1 && (
<Field
label="Transformer"
className={css`
margin-bottom: 0;
`}
>
<Select
options={transformationOptions}
value={transformId}
onChange={this.onTransformationChange}
width={15}
/>
</Field>
)}
{choices.length > 1 && (
<Field
label="Select result"
className={css`
margin-bottom: 0;
`}
>
<Select options={choices} value={dataFrameIndex} onChange={this.onSelectedFrameChanged} />
</Field>
)}
</div>
<div className={cx(styles.options, styles.dataDisplayOptions)}>
<QueryOperationRow title={'Data display options'} isOpen={false}>
{panelTransformations && panelTransformations.length > 0 && (transformId as any) !== 'join by time' && (
<div className="gf-form-inline">
<Switch
tooltip="Data shown in the table will be transformed using transformations defined in the panel"
label="Apply panel transformations"
labelClass="width-12"
checked={!!options.withTransforms}
onChange={() => onOptionsChange({ ...options, withTransforms: !options.withTransforms })}
/>
</div>
)}
<div className="gf-form-inline">
<Switch
tooltip="Data shown in the table will have panel field configuration applied, for example units or display name"
label="Apply field configuration"
labelClass="width-12"
checked={!!options.withFieldConfig}
onChange={() => onOptionsChange({ ...options, withFieldConfig: !options.withFieldConfig })}
/>
</div>
</QueryOperationRow>
</div>
</div>
<div className={styles.options}>
<Button variant="primary" onClick={() => this.exportCsv(dataFrames[dataFrameIndex])}>
Download CSV
</Button>
</div>
</div>
<div style={{ flexGrow: 1 }}>
<AutoSizer>
{({ width, height }) => {
......
......@@ -28,6 +28,7 @@ import { getPanelInspectorStyles } from './styles';
import { StoreState } from 'app/types';
import { InspectDataTab } from './InspectDataTab';
import { supportsDataQuery } from '../PanelEditor/utils';
import { GetDataOptions } from '../../state/PanelQueryRunner';
interface OwnProps {
dashboard: DashboardModel;
......@@ -62,6 +63,8 @@ interface State {
metaDS?: DataSourceApi;
// drawer width
drawerWidth: string;
withTransforms: boolean;
withFieldConfig: boolean;
}
export class PanelInspectorUnconnected extends PureComponent<Props, State> {
......@@ -76,6 +79,8 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
data: [],
currentTab: props.defaultTab ?? InspectTab.Data,
drawerWidth: '50%',
withTransforms: true,
withFieldConfig: false,
};
}
......@@ -87,8 +92,12 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
}
}
componentDidUpdate(prevProps: Props) {
if (prevProps.plugin !== this.props.plugin) {
componentDidUpdate(prevProps: Props, prevState: State) {
if (
prevProps.plugin !== this.props.plugin ||
this.state.withTransforms !== prevState.withTransforms ||
this.state.withFieldConfig !== prevState.withFieldConfig
) {
this.init();
}
}
......@@ -99,11 +108,15 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
*/
init() {
const { plugin, panel } = this.props;
const { withTransforms, withFieldConfig } = this.state;
if (plugin && !plugin.meta.skipDataQuery) {
if (this.querySubscription) {
this.querySubscription.unsubscribe();
}
this.querySubscription = panel
.getQueryRunner()
.getData()
.getData({ withTransforms, withFieldConfig })
.subscribe({
next: data => this.onUpdateData(data),
});
......@@ -164,6 +177,9 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
onSelectTab = (item: SelectableValue<InspectTab>) => {
this.setState({ currentTab: item.value || InspectTab.Data });
};
onDataTabOptionsChange = (options: GetDataOptions) => {
this.setState({ withTransforms: !!options.withTransforms, withFieldConfig: !!options.withFieldConfig });
};
renderMetadataInspector() {
const { metaDS, data } = this.state;
......@@ -174,8 +190,20 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
}
renderDataTab() {
const { last, isLoading } = this.state;
return <InspectDataTab data={last.series} isLoading={isLoading} />;
const { last, isLoading, withFieldConfig, withTransforms } = this.state;
return (
<InspectDataTab
dashboard={this.props.dashboard}
panel={this.props.panel}
data={last.series}
isLoading={isLoading}
options={{
withFieldConfig,
withTransforms,
}}
onOptionsChange={this.onDataTabOptionsChange}
/>
);
}
renderErrorTab(error?: DataQueryError) {
......
......@@ -41,9 +41,6 @@ export const getPanelInspectorStyles = stylesFactory(() => {
dataFrameSelect: css`
flex-grow: 2;
`,
downloadCsv: css`
margin-left: 16px;
`,
tabContent: css`
height: 100%;
display: flex;
......@@ -55,5 +52,27 @@ export const getPanelInspectorStyles = stylesFactory(() => {
height: 100%;
width: 100%;
`,
actionsWrapper: css`
display: flex;
flex-wrap: wrap;
`,
leftActions: css`
display: flex;
flex-grow: 1;
`,
options: css`
margin-top: 19px;
`,
dataDisplayOptions: css`
flex-grow: 1;
min-width: 300px;
margin-right: ${config.theme.spacing.sm};
`,
selects: css`
display: flex;
> * {
margin-right: ${config.theme.spacing.sm};
}
`,
};
});
......@@ -17,7 +17,7 @@ export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardMod
const panel = dashboard.initEditPanel(sourcePanel);
const queryRunner = panel.getQueryRunner();
const querySubscription = queryRunner.getData(false).subscribe({
const querySubscription = queryRunner.getData({ withTransforms: false }).subscribe({
next: (data: PanelData) => dispatch(setEditorPanelData(data)),
});
......
......@@ -78,7 +78,7 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
// subscribe to data events
const queryRunner = panel.getQueryRunner();
this.querySubscription = queryRunner.getData(false).subscribe({
this.querySubscription = queryRunner.getData({ withTransforms: false }).subscribe({
next: (data: PanelData) => this.onPanelDataUpdate(data),
});
}
......
......@@ -50,7 +50,7 @@ interface State {
export class QueriesTab extends PureComponent<Props, State> {
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
backendSrv = backendSrv;
querySubscription: Unsubscribable;
querySubscription: Unsubscribable | null;
state: State = {
isLoadingHelp: false,
......@@ -71,7 +71,7 @@ export class QueriesTab extends PureComponent<Props, State> {
const { panel } = this.props;
const queryRunner = panel.getQueryRunner();
this.querySubscription = queryRunner.getData(false).subscribe({
this.querySubscription = queryRunner.getData({ withTransforms: false }).subscribe({
next: (data: PanelData) => this.onPanelDataUpdate(data),
});
......
......@@ -230,6 +230,7 @@ describe('PanelQueryRunner', () => {
ctx => {
it('should apply when transformations are set', async () => {
const spy = jest.spyOn(grafanaData, 'transformDataFrame');
spy.mockClear();
ctx.runner.getData().subscribe({
next: (data: PanelData) => {
......@@ -246,4 +247,48 @@ describe('PanelQueryRunner', () => {
getTransformations: () => [{}],
}
);
describeQueryRunnerScenario(
'getData',
ctx => {
it('should not apply transformations when transform option is false', async () => {
const spy = jest.spyOn(grafanaData, 'transformDataFrame');
spy.mockClear();
ctx.runner.getData({ withTransforms: false }).subscribe({
next: (data: PanelData) => {
return data;
},
});
expect(spy).not.toBeCalled();
});
it('should not apply field config when applyFieldConfig option is false', async () => {
const spy = jest.spyOn(grafanaData, 'applyFieldOverrides');
spy.mockClear();
ctx.runner.getData({ withFieldConfig: false }).subscribe({
next: (data: PanelData) => {
return data;
},
});
expect(spy).not.toBeCalled();
});
},
{
getFieldOverrideOptions: () => ({
fieldConfig: {
defaults: {
unit: 'm/s',
},
// @ts-ignore
overrides: [],
},
replaceVariables: v => v,
theme: {} as GrafanaTheme,
}),
// @ts-ignore
getTransformations: () => [{}],
}
);
});
......@@ -51,6 +51,15 @@ function getNextRequestId() {
return 'Q' + counter++;
}
export interface GetDataOptions {
withTransforms?: boolean;
withFieldConfig?: boolean;
}
const DEFAULT_GET_DATA_OPTIONS: GetDataOptions = {
withTransforms: true,
withFieldConfig: true,
};
export class PanelQueryRunner {
private subject?: ReplaySubject<PanelData>;
private subscription?: Unsubscribable;
......@@ -66,37 +75,39 @@ export class PanelQueryRunner {
/**
* Returns an observable that subscribes to the shared multi-cast subject (that reply last result).
*/
getData(transform = true): Observable<PanelData> {
getData(options: GetDataOptions = DEFAULT_GET_DATA_OPTIONS): Observable<PanelData> {
const { withFieldConfig, withTransforms } = options;
return this.subject.pipe(
map((data: PanelData) => {
let processedData = data;
// Apply transformations
if (transform) {
// Apply transformation
if (withTransforms) {
const transformations = this.dataConfigSource.getTransformations();
if (transformations && transformations.length > 0) {
processedData = {
...processedData,
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
series: transformDataFrame(transformations, data.series),
};
}
}
// Apply field defaults & overrides
const fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
if (fieldConfig) {
processedData = {
...processedData,
series: applyFieldOverrides({
timeZone: this.timeZone,
autoMinMax: true,
data: processedData.series,
...fieldConfig,
}),
};
if (withFieldConfig) {
// Apply field defaults & overrides
const fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
if (fieldConfig) {
processedData = {
...processedData,
series: applyFieldOverrides({
timeZone: this.timeZone,
autoMinMax: true,
data: processedData.series,
...fieldConfig,
}),
};
}
}
return processedData;
......
......@@ -35,7 +35,7 @@ export function runSharedRequest(options: QueryRunnerOptions): Observable<PanelD
}
const listenToRunner = listenToPanel.getQueryRunner();
const subscription = listenToRunner.getData(false).subscribe({
const subscription = listenToRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
next: (data: PanelData) => {
subscriber.next(data);
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment