grafana/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx
2023-09-14 12:45:13 +02:00

248 lines
7.9 KiB
TypeScript

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