The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx

247 lines
7.9 KiB

import { isEqual } from 'lodash';
import React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import {
SceneComponentProps,
SceneDataTransformer,
sceneGraph,
SceneGridItem,
SceneGridItemStateLike,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
SceneQueryRunner,
sceneUtils,
VizPanel,
} from '@grafana/scenes';
import { Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';
import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles2 } from 'app/features/inspector/styles';
import { InspectTab } from 'app/features/inspector/types';
import { getPrettyJSON } from 'app/features/inspector/utils/utils';
import { reportPanelInspectInteraction } from 'app/features/search/page/reporting';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames';
export interface InspectJsonTabState extends SceneObjectState {
panelRef: SceneObjectRef<VizPanel>;
source: ShowContent;
jsonText: string;
onClose: () => void;
}
export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
public constructor(state: Omit<InspectJsonTabState, 'source' | 'jsonText'>) {
super({
...state,
source: 'panel-json',
jsonText: getJsonText('panel-json', state.panelRef.resolve()),
});
}
public getTabLabel() {
return t('dashboard.inspect.json-tab', 'JSON');
}
public getTabValue() {
return InspectTab.JSON;
}
public getOptions(): Array<SelectableValue<ShowContent>> {
const panel = this.state.panelRef.resolve();
const dataProvider = panel.state.$data;
const options: Array<SelectableValue<ShowContent>> = [
{
label: t('dashboard.inspect-json.panel-json-label', 'Panel JSON'),
description: t(
'dashboard.inspect-json.panel-json-description',
'The model saved in the dashboard JSON that configures how everything works.'
),
value: 'panel-json',
},
];
if (dataProvider) {
options.push({
label: t('dashboard.inspect-json.panel-data-label', 'Panel data'),
description: t(
'dashboard.inspect-json.panel-data-description',
'The raw model passed to the panel visualization'
),
value: 'panel-data',
});
options.push({
label: t('dashboard.inspect-json.dataframe-label', 'DataFrame JSON (from Query)'),
description: t(
'dashboard.inspect-json.dataframe-description',
'Raw data without transformations and field config applied. '
),
value: 'data-frames',
});
}
return options;
}
public onChangeSource = (value: SelectableValue<ShowContent>) => {
this.setState({ source: value.value!, jsonText: getJsonText(value.value!, this.state.panelRef.resolve()) });
};
public onApplyChange = () => {
const panel = this.state.panelRef.resolve();
const dashboard = getDashboardSceneFor(panel);
const jsonObj = JSON.parse(this.state.jsonText);
const panelModel = new PanelModel(jsonObj);
const gridItem = buildGridItemForPanel(panelModel);
const newState = sceneUtils.cloneSceneObjectState(gridItem.state);
if (!(panel.parent instanceof SceneGridItem) || !(gridItem instanceof SceneGridItem)) {
console.error('Cannot update state of panel', panel, gridItem);
return;
}
this.state.onClose();
if (!dashboard.state.isEditing) {
dashboard.onEnterEditMode();
}
panel.parent.setState(newState);
//Report relevant updates
reportPanelInspectInteraction(InspectTab.JSON, 'apply', {
panel_type_changed: panel.state.pluginId !== panelModel.type,
panel_id_changed: getPanelIdForVizPanel(panel) !== panelModel.id,
panel_grid_pos_changed: hasGridPosChanged(panel.parent.state, newState),
panel_targets_changed: hasQueriesChanged(getQueryRunnerFor(panel), getQueryRunnerFor(newState.$data)),
});
};
public onCodeEditorBlur = (value: string) => {
this.setState({ jsonText: value });
};
public isEditable() {
if (this.state.source !== 'panel-json') {
return false;
}
const panel = this.state.panelRef.resolve();
// Only support normal grid items for now and not repeated items
if (!(panel.parent instanceof SceneGridItem)) {
return false;
}
const dashboard = getDashboardSceneFor(panel);
return dashboard.state.meta.canEdit;
}
static Component = ({ model }: SceneComponentProps<InspectJsonTab>) => {
const { source: show, jsonText } = model.useState();
const styles = useStyles2(getPanelInspectorStyles2);
const options = model.getOptions();
return (
<div className={styles.wrap}>
<div className={styles.toolbar} aria-label={selectors.components.PanelInspector.Json.content}>
<Field label={t('dashboard.inspect-json.select-source', 'Select source')} className="flex-grow-1">
<Select
inputId="select-source-dropdown"
options={options}
value={options.find((v) => v.value === show) ?? options[0].value}
onChange={model.onChangeSource}
/>
</Field>
{model.isEditable() && (
<Button className={styles.toolbarItem} onClick={model.onApplyChange}>
Apply
</Button>
)}
</div>
<div className={styles.content}>
<AutoSizer disableWidth>
{({ height }) => (
<CodeEditor
width="100%"
height={height}
language="json"
showLineNumbers={true}
showMiniMap={jsonText.length > 100}
value={jsonText}
readOnly={!model.isEditable()}
onBlur={model.onCodeEditorBlur}
/>
)}
</AutoSizer>
</div>
</div>
);
};
}
function getJsonText(show: ShowContent, panel: VizPanel): string {
let objToStringify: object = {};
switch (show) {
case 'panel-json': {
reportPanelInspectInteraction(InspectTab.JSON, 'panelData');
if (panel.parent instanceof SceneGridItem || panel.parent instanceof PanelRepeaterGridItem) {
objToStringify = gridItemToPanel(panel.parent);
}
break;
}
case 'panel-data': {
reportPanelInspectInteraction(InspectTab.JSON, 'panelJSON');
const dataProvider = sceneGraph.getData(panel);
if (dataProvider.state.data) {
objToStringify = panel.applyFieldConfig(dataProvider.state.data);
}
break;
}
case 'data-frames': {
reportPanelInspectInteraction(InspectTab.JSON, 'dataFrame');
const dataProvider = sceneGraph.getData(panel);
if (dataProvider.state.data) {
// Get raw untransformed data
if (dataProvider instanceof SceneDataTransformer && dataProvider.state.$data?.state.data) {
objToStringify = getPanelDataFrames(dataProvider.state.$data!.state.data);
} else {
objToStringify = getPanelDataFrames(dataProvider.state.data);
}
}
}
}
return getPrettyJSON(objToStringify);
}
function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) {
return a.x !== b.x || a.y !== b.y || a.width !== b.width || a.height !== b.height;
}
function hasQueriesChanged(a: SceneQueryRunner | undefined, b: SceneQueryRunner | undefined) {
if (a === undefined || b === undefined) {
return false;
}
return !isEqual(a.state.queries, b.state.queries);
}