mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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"]
|
||||
],
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
37
public/app/features/inspector/utils/utils.ts
Normal file
37
public/app/features/inspector/utils/utils.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user