mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9e24c0944f
commit
f23ecc40b4
@ -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);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user