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>
This commit is contained in:
Torkel Ödegaard 2020-05-13 13:03:34 +02:00 committed by GitHub
parent 9e24c0944f
commit f23ecc40b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 233 additions and 60 deletions

View File

@ -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

View File

@ -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};
}
`,
};

View File

@ -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}>

View File

@ -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 }) => {

View File

@ -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) {

View File

@ -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};
}
`,
};
});

View File

@ -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)),
});

View File

@ -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),
});
}

View File

@ -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),
});

View File

@ -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: () => [{}],
}
);
});

View File

@ -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;

View File

@ -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);
},