DashboardScene: Inspect / Json tab (#74701)

* DashboardScene: Inspect / Json tab

* Fixing behaviors and writing tests

* Progress

* limit options based on data provider

* Fixes

* Add tracking

* Remove unused function

* Remove unused function

* Fix test

* Update

* Move utils function

* Rename to source
This commit is contained in:
Torkel Ödegaard
2023-09-14 12:17:04 +02:00
committed by GitHub
parent 0451e59134
commit 5e9f252962
13 changed files with 563 additions and 81 deletions

View File

@@ -1789,6 +1789,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],

View File

@@ -7,14 +7,18 @@ import {
SceneDataTransformer,
sceneGraph,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
VizPanel,
} from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { InspectTab } from 'app/features/inspector/types';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { InspectDataTab as InspectDataTabOld } from '../../inspector/InspectDataTab';
import { InspectTabState } from './types';
export interface InspectDataTabState extends InspectTabState {
export interface InspectDataTabState extends SceneObjectState {
panelRef: SceneObjectRef<VizPanel>;
options: GetDataOptions;
}
@@ -29,6 +33,14 @@ export class InspectDataTab extends SceneObjectBase<InspectDataTabState> {
});
}
public getTabLabel() {
return t('dashboard.inspect.data-tab', 'Data');
}
public getTabValue() {
return InspectTab.Data;
}
public onOptionsChange = (options: GetDataOptions) => {
this.setState({ options });
};

View File

@@ -0,0 +1,162 @@
import { FieldType, getDefaultTimeRange, LoadingState, standardTransformersRegistry, toDataFrame } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime';
import {
SceneCanvasText,
SceneDataNode,
SceneDataTransformer,
SceneGridItem,
SceneGridLayout,
SceneObjectRef,
VizPanel,
} from '@grafana/scenes';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils';
import { findVizPanelByKey } from '../utils/utils';
import { InspectJsonTab } from './InspectJsonTab';
standardTransformersRegistry.setInit(getStandardTransformers);
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
describe('InspectJsonTab', () => {
it('Can show panel json', async () => {
const { tab } = await buildTestScene();
const obj = JSON.parse(tab.state.jsonText);
expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 });
expect(tab.isEditable()).toBe(true);
});
it('Can show panel data with field config', async () => {
const { tab } = await buildTestScene();
tab.onChangeSource({ value: 'panel-data' });
expect(tab.isEditable()).toBe(false);
const obj = JSON.parse(tab.state.jsonText);
expect(obj.series.length).toBe(1);
expect(obj.state).toBe(LoadingState.Done);
// verify scopedVars __sceneObject is filtered out
expect(obj.request.scopedVars.__sceneObject).toEqual('Filtered out in JSON serialization');
});
it('Can show raw data frames', async () => {
const { tab } = await buildTestScene();
tab.onChangeSource({ value: 'data-frames' });
const obj = JSON.parse(tab.state.jsonText);
expect(Array.isArray(obj)).toBe(true);
expect(obj[0].schema.fields.length).toBe(1);
expect(tab.isEditable()).toBe(false);
});
it('Can update model', async () => {
const { tab, panel, scene } = await buildTestScene();
tab.onCodeEditorBlur(`{
"id": 12,
"type": "table",
"title": "New title",
"gridPos": {
"x": 1,
"y": 2,
"w": 3,
"h": 4
},
"options": {},
"fieldConfig": {},
"transformations": [],
"transparent": false
}`);
tab.onApplyChange();
const panel2 = findVizPanelByKey(scene, panel.state.key)!;
expect(panel2.state.title).toBe('New title');
expect((panel2.parent as SceneGridItem).state.width!).toBe(3);
expect(tab.state.onClose).toHaveBeenCalled();
});
});
async function buildTestScene() {
const panel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-12',
$data: new SceneDataTransformer({
transformations: [
{
id: 'reduce',
options: {
reducers: ['last'],
},
},
],
$data: new SceneDataNode({
data: {
state: LoadingState.Done,
series: [
toDataFrame({
fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }],
}),
],
timeRange: getDefaultTimeRange(),
request: {
app: 'dashboard',
requestId: 'request-id',
dashboardUID: 'asd',
interval: '1s',
panelId: 1,
range: getDefaultTimeRange(),
targets: [],
timezone: 'utc',
intervalMs: 1000,
startTime: 1,
scopedVars: {
__sceneObject: { value: new SceneCanvasText({ text: 'asd' }) },
},
},
},
}),
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new SceneGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
});
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
const tab = new InspectJsonTab({
panelRef: new SceneObjectRef(panel),
onClose: jest.fn(),
});
return { scene, tab, panel };
}

View File

@@ -1,11 +1,248 @@
import { isEqual } from 'lodash';
import React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
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 { InspectTabState } from './types';
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(gridItem.state.body)),
});
};
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;
}
export class InspectJsonTab extends SceneObjectBase<InspectTabState> {
static Component = ({ model }: SceneComponentProps<InspectJsonTab>) => {
return <div>JSON</div>;
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);
}

View File

@@ -1,12 +1,31 @@
import React from 'react';
import { SceneComponentProps, sceneGraph, SceneObjectBase } from '@grafana/scenes';
import {
SceneComponentProps,
sceneGraph,
SceneObjectBase,
SceneObjectState,
SceneObjectRef,
VizPanel,
} from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { InspectTab } from 'app/features/inspector/types';
import { InspectStatsTab as OldInspectStatsTab } from '../../inspector/InspectStatsTab';
import { InspectTabState } from './types';
export interface InspectDataTabState extends SceneObjectState {
panelRef: SceneObjectRef<VizPanel>;
}
export class InspectStatsTab extends SceneObjectBase<InspectDataTabState> {
public getTabLabel() {
return t('dashboard.inspect.stats-tab', 'Stats');
}
public getTabValue() {
return InspectTab.Stats;
}
export class InspectStatsTab extends SceneObjectBase<InspectTabState> {
static Component = ({ model }: SceneComponentProps<InspectStatsTab>) => {
const data = sceneGraph.getData(model.state.panelRef.resolve()).useState();
const timeRange = sceneGraph.getTimeRange(model.state.panelRef.resolve());

View File

@@ -7,25 +7,23 @@ import {
SceneComponentProps,
SceneObjectBase,
SceneObjectState,
SceneObject,
sceneGraph,
VizPanel,
SceneObjectRef,
} from '@grafana/scenes';
import { Alert, Drawer, Tab, TabsBar } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { InspectTab } from 'app/features/inspector/types';
import { InspectDataTab } from './InspectDataTab';
import { InspectJsonTab } from './InspectJsonTab';
import { InspectStatsTab } from './InspectStatsTab';
import { InspectTabState } from './types';
import { SceneInspectTab } from './types';
interface PanelInspectDrawerState extends SceneObjectState {
tabs?: Array<SceneObject<InspectTabState>>;
tabs?: SceneInspectTab[];
panelRef: SceneObjectRef<VizPanel>;
pluginNotLoaded?: boolean;
canEdit?: boolean;
}
export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> {
@@ -45,24 +43,22 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
const panelRef = this.state.panelRef;
const panel = panelRef.resolve();
const plugin = panel.getPlugin();
const tabs: Array<SceneObject<InspectTabState>> = [];
const tabs: SceneInspectTab[] = [];
if (plugin) {
if (supportsDataQuery(plugin)) {
tabs.push(
new InspectDataTab({ panelRef, label: t('dashboard.inspect.data-tab', 'Data'), value: InspectTab.Data })
);
tabs.push(
new InspectStatsTab({ panelRef, label: t('dashboard.inspect.stats-tab', 'Stats'), value: InspectTab.Stats })
);
if (!plugin) {
if (retry < 2000) {
setTimeout(() => this.buildTabs(retry + 100), 100);
} else {
this.setState({ pluginNotLoaded: true });
}
} else if (retry < 2000) {
setTimeout(() => this.buildTabs(retry + 100), 100);
} else {
this.setState({ pluginNotLoaded: true });
}
tabs.push(new InspectJsonTab({ panelRef, label: t('dashboard.inspect.json-tab', 'JSON'), value: InspectTab.JSON }));
if (supportsDataQuery(plugin)) {
tabs.push(new InspectDataTab({ panelRef }));
tabs.push(new InspectStatsTab({ panelRef }));
}
tabs.push(new InspectJsonTab({ panelRef, onClose: this.onClose }));
this.setState({ tabs });
}
@@ -87,23 +83,23 @@ function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>
}
const urlTab = queryParams.get('inspectTab');
const currentTab = tabs.find((tab) => tab.state.value === urlTab) ?? tabs[0];
const currentTab = tabs.find((tab) => tab.getTabValue() === urlTab) ?? tabs[0];
return (
<Drawer
title={model.getDrawerTitle()}
scrollableContent
onClose={model.onClose}
size="lg"
size="md"
tabs={
<TabsBar>
{tabs.map((tab) => {
return (
<Tab
key={tab.state.key!}
label={tab.state.label}
label={tab.getTabLabel()}
active={tab === currentTab}
href={locationUtil.getUrlForPartial(location, { inspectTab: tab.state.value })}
href={locationUtil.getUrlForPartial(location, { inspectTab: tab.getTabValue() })}
/>
);
})}
@@ -115,7 +111,7 @@ function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>
Make sure the panel you want to inspect is visible and has been displayed before opening inspect.
</Alert>
)}
{currentTab.Component && <currentTab.Component model={currentTab} />}
{currentTab && currentTab.Component && <currentTab.Component model={currentTab} />}
</Drawer>
);
}

View File

@@ -1,8 +1,7 @@
import { SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes';
import { SceneObject, SceneObjectState } from '@grafana/scenes';
import { InspectTab } from 'app/features/inspector/types';
export interface InspectTabState extends SceneObjectState {
label: string;
value: InspectTab;
panelRef: SceneObjectRef<VizPanel>;
export interface SceneInspectTab<T extends SceneObjectState = SceneObjectState> extends SceneObject<T> {
getTabValue(): InspectTab;
getTabLabel(): string;
}

View File

@@ -5,6 +5,7 @@ import { NavModelItem, UrlQueryMap } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import {
getUrlSyncManager,
SceneFlexLayout,
SceneGridItem,
SceneGridLayout,
SceneObject,
@@ -14,6 +15,7 @@ import {
SceneObjectStateChangedEvent,
sceneUtils,
} from '@grafana/scenes';
import { DashboardMeta } from 'app/types';
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
@@ -29,6 +31,8 @@ export interface DashboardSceneState extends SceneObjectState {
controls?: SceneObject[];
isEditing?: boolean;
isDirty?: boolean;
/** meta flags */
meta: DashboardMeta;
/** Panel to inspect */
inspectPanelKey?: string;
/** Panel to view in full screen */
@@ -57,8 +61,13 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
*/
private _changeTrackerSub?: Unsubscribable;
public constructor(state: DashboardSceneState) {
super(state);
public constructor(state: Partial<DashboardSceneState>) {
super({
title: 'Dashboard',
meta: {},
body: state.body ?? new SceneFlexLayout({ children: [] }),
...state,
});
this.addActivationHandler(() => this._activationHandler());
}
@@ -156,12 +165,18 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
SceneObjectStateChangedEvent,
(event: SceneObjectStateChangedEvent) => {
if (event.payload.changedObject instanceof SceneGridItem) {
this.setState({ isDirty: true });
this.setIsDirty();
}
}
);
}
private setIsDirty() {
if (!this.state.isDirty) {
this.setState({ isDirty: true });
}
}
private stopTrackingChanges() {
this._changeTrackerSub?.unsubscribe();
}

View File

@@ -181,6 +181,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
return new DashboardScene({
title: oldModel.title,
uid: oldModel.uid,
meta: oldModel.meta,
body: new SceneGridLayout({
isLazy: true,
children: createSceneObjectsForPanels(oldModel.panels),

View File

@@ -1,6 +1,15 @@
import { UrlQueryMap, urlUtil } from '@grafana/data';
import { locationSearchToObject } from '@grafana/runtime';
import { MultiValueVariable, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
import {
MultiValueVariable,
SceneDataTransformer,
sceneGraph,
SceneObject,
SceneQueryRunner,
VizPanel,
} from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene';
export function getVizPanelKeyForPanelId(panelId: number) {
return `panel-${panelId}`;
@@ -113,3 +122,28 @@ export function getMultiVariableValues(variable: MultiValueVariable) {
texts: Array.isArray(text) ? text : [text],
};
}
export function getDashboardSceneFor(sceneObject: SceneObject): DashboardScene {
const root = sceneObject.getRoot();
if (root instanceof DashboardScene) {
return root;
}
throw new Error('SceneObject root is not a DashboardScene');
}
export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQueryRunner | undefined {
if (!sceneObject) {
return undefined;
}
if (sceneObject.state.$data instanceof SceneQueryRunner) {
return sceneObject.state.$data;
}
if (sceneObject.state.$data instanceof SceneDataTransformer) {
return getQueryRunnerFor(sceneObject.state.$data);
}
return undefined;
}

View File

@@ -5,13 +5,14 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
const getStyles = (theme: GrafanaTheme2) => css`
margin: 0;
margin-left: ${theme.spacing(2)};
padding: ${theme.spacing(0, 2)};
font-size: ${theme.typography.bodySmall.fontSize};
color: ${theme.colors.text.secondary};
overflow: hidden;
text-overflow: ellipsis;
`;
export const DetailText = ({ children }: React.PropsWithChildren<{}>) => {
const collapsedTextStyles = useStyles2(getStyles);
return <p className={collapsedTextStyles}>{children}</p>;
return <div className={collapsedTextStyles}>{children}</div>;
};

View File

@@ -17,6 +17,7 @@ import { getPanelInspectorStyles2 } from '../inspector/styles';
import { reportPanelInspectInteraction } from '../search/page/reporting';
import { InspectTab } from './types';
import { getPrettyJSON } from './utils/utils';
enum ShowContent {
PanelJSON = 'panel',
@@ -187,38 +188,3 @@ async function getJSONObject(show: ShowContent, panel?: PanelModel, data?: Panel
return { note: t('dashboard.inspect-json.unknown', 'Unknown Object: {{show}}', { show }) };
}
function getPrettyJSON(obj: unknown): string {
let r = '';
try {
r = JSON.stringify(obj, getCircularReplacer(), 2);
} catch (e) {
if (
e instanceof Error &&
(e.toString().includes('RangeError') || e.toString().includes('allocation size overflow'))
) {
appEvents.emit(AppEvents.alertError, [e.toString(), 'Cannot display JSON, the object is too big.']);
} else {
appEvents.emit(AppEvents.alertError, [e instanceof Error ? e.toString() : e]);
}
}
return r;
}
function getCircularReplacer() {
const seen = new WeakSet();
return (key: string, value: unknown) => {
if (key === '__dataContext') {
return 'Filtered out in JSON serialization';
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}

View File

@@ -0,0 +1,37 @@
import { AppEvents } from '@grafana/data';
import appEvents from 'app/core/app_events';
export function getPrettyJSON(obj: unknown): string {
let r = '';
try {
r = JSON.stringify(obj, getCircularReplacer(), 2);
} catch (e) {
if (
e instanceof Error &&
(e.toString().includes('RangeError') || e.toString().includes('allocation size overflow'))
) {
appEvents.emit(AppEvents.alertError, [e.toString(), 'Cannot display JSON, the object is too big.']);
} else {
appEvents.emit(AppEvents.alertError, [e instanceof Error ? e.toString() : e]);
}
}
return r;
}
function getCircularReplacer() {
const seen = new WeakSet();
return (key: string, value: unknown) => {
if (key === '__dataContext' || key === '__sceneObject') {
return 'Filtered out in JSON serialization';
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}