mirror of https://github.com/grafana/grafana
Dashboard Scene: All panel menu items available (#81678)
* WIP: removing panel functionality * wip * Deleting works with old model and with unified alerting * Add shortcut for removing panel * Add duplicate panel functionality; improve remove panel logic * Copy and new alert rule * Hide legend * WIP: Help wizard * Fix PanelMenuBehavior tests * Got help wizard to work in scenes * Fix HelpWizard and SupportSnapshotService tests * Use object for writing styles * betterer * Fix create lib panel * PanelRepeaterItem should be duplicated * Share randomizer from dashboard-scenes * share randomizer * Fix import * Update error message * Fix test * When duplicating PanelRepeaterGridItem's child use PanelRepeaterGridItem.state.source * Don't use getResultsStream --------- Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>pull/82080/head
parent
b5f26560c2
commit
00aa876e46
@ -0,0 +1,94 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import React from 'react'; |
||||
|
||||
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; |
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; |
||||
import { |
||||
SceneGridItem, |
||||
SceneGridLayout, |
||||
SceneQueryRunner, |
||||
SceneTimeRange, |
||||
VizPanel, |
||||
VizPanelMenu, |
||||
} from '@grafana/scenes'; |
||||
|
||||
import { DashboardScene } from '../../scene/DashboardScene'; |
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; |
||||
import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; |
||||
|
||||
import { HelpWizard } from './HelpWizard'; |
||||
|
||||
async function setup() { |
||||
const { panel } = await buildTestScene(); |
||||
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
||||
|
||||
return render(<HelpWizard panel={panel} onClose={() => {}} />); |
||||
} |
||||
describe('SupportSnapshot', () => { |
||||
it('Can render', async () => { |
||||
setup(); |
||||
expect(await screen.findByRole('button', { name: 'Dashboard (3.50 KiB)' })).toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
|
||||
async function buildTestScene() { |
||||
const menu = new VizPanelMenu({ |
||||
$behaviors: [panelMenuBehavior], |
||||
}); |
||||
|
||||
const panel = new VizPanel({ |
||||
title: 'Panel A', |
||||
pluginId: 'timeseries', |
||||
key: 'panel-12', |
||||
menu, |
||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], |
||||
$data: new SceneQueryRunner({ |
||||
data: { |
||||
state: LoadingState.Done, |
||||
series: [ |
||||
toDataFrame({ |
||||
name: 'http_requests_total', |
||||
fields: [ |
||||
{ name: 'Time', type: FieldType.time, values: [1, 2, 3] }, |
||||
{ name: 'Value', type: FieldType.number, values: [11, 22, 33] }, |
||||
], |
||||
}), |
||||
], |
||||
timeRange: getDefaultTimeRange(), |
||||
}, |
||||
datasource: { uid: 'my-uid' }, |
||||
queries: [{ query: 'QueryA', refId: 'A' }], |
||||
}), |
||||
}); |
||||
|
||||
const scene = new DashboardScene({ |
||||
title: 'My dashboard', |
||||
uid: 'dash-1', |
||||
tags: ['database', 'panel'], |
||||
$timeRange: new SceneTimeRange({ |
||||
from: 'now-5m', |
||||
to: 'now', |
||||
timeZone: 'Africa/Abidjan', |
||||
}), |
||||
meta: { |
||||
canEdit: true, |
||||
isEmbedded: false, |
||||
}, |
||||
body: new SceneGridLayout({ |
||||
children: [ |
||||
new SceneGridItem({ |
||||
key: 'griditem-1', |
||||
x: 0, |
||||
y: 0, |
||||
width: 10, |
||||
height: 12, |
||||
body: panel, |
||||
}), |
||||
], |
||||
}), |
||||
}); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
return { scene, panel, menu }; |
||||
} |
||||
@ -0,0 +1,229 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { useMemo, useEffect } from 'react'; |
||||
import AutoSizer from 'react-virtualized-auto-sizer'; |
||||
|
||||
import { GrafanaTheme2, FeatureState } from '@grafana/data'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { VizPanel } from '@grafana/scenes'; |
||||
import { |
||||
Drawer, |
||||
Tab, |
||||
TabsBar, |
||||
CodeEditor, |
||||
useStyles2, |
||||
Field, |
||||
HorizontalGroup, |
||||
InlineSwitch, |
||||
Button, |
||||
Spinner, |
||||
Alert, |
||||
FeatureBadge, |
||||
Select, |
||||
ClipboardButton, |
||||
Icon, |
||||
Stack, |
||||
} from '@grafana/ui'; |
||||
import { contextSrv } from 'app/core/services/context_srv'; |
||||
import { AccessControlAction } from 'app/types'; |
||||
|
||||
import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; |
||||
|
||||
interface Props { |
||||
panel: VizPanel; |
||||
onClose: () => void; |
||||
} |
||||
|
||||
export function HelpWizard({ panel, onClose }: Props) { |
||||
const styles = useStyles2(getStyles); |
||||
const service = useMemo(() => new SupportSnapshotService(panel), [panel]); |
||||
const plugin = panel.getPlugin(); |
||||
|
||||
const { |
||||
currentTab, |
||||
loading, |
||||
error, |
||||
options, |
||||
showMessage, |
||||
snapshotSize, |
||||
markdownText, |
||||
snapshotText, |
||||
randomize, |
||||
panelTitle, |
||||
scene, |
||||
} = service.useState(); |
||||
|
||||
useEffect(() => { |
||||
service.buildDebugDashboard(); |
||||
}, [service, plugin, randomize]); |
||||
|
||||
if (!plugin) { |
||||
return null; |
||||
} |
||||
|
||||
const tabs = [ |
||||
{ label: 'Snapshot', value: SnapshotTab.Support }, |
||||
{ label: 'Data', value: SnapshotTab.Data }, |
||||
]; |
||||
|
||||
const hasSupportBundleAccess = |
||||
config.supportBundlesEnabled && contextSrv.hasPermission(AccessControlAction.ActionSupportBundlesCreate); |
||||
|
||||
return ( |
||||
<Drawer |
||||
title={`Get help with this panel`} |
||||
size="lg" |
||||
onClose={onClose} |
||||
subtitle={ |
||||
<Stack direction="column" gap={1}> |
||||
<Stack direction="row" gap={1}> |
||||
<FeatureBadge featureState={FeatureState.beta} /> |
||||
<a |
||||
href="https://grafana.com/docs/grafana/latest/troubleshooting/" |
||||
target="blank" |
||||
className="external-link" |
||||
rel="noopener noreferrer" |
||||
> |
||||
Troubleshooting docs <Icon name="external-link-alt" /> |
||||
</a> |
||||
</Stack> |
||||
<span className="muted"> |
||||
To request troubleshooting help, send a snapshot of this panel to Grafana Labs Technical Support. The |
||||
snapshot contains query response data and panel settings. |
||||
</span> |
||||
{hasSupportBundleAccess && ( |
||||
<span className="muted"> |
||||
You can also retrieve a support bundle containing information concerning your Grafana instance and |
||||
configured datasources in the <a href="/support-bundles">support bundles section</a>. |
||||
</span> |
||||
)} |
||||
</Stack> |
||||
} |
||||
tabs={ |
||||
<TabsBar> |
||||
{tabs.map((t, index) => ( |
||||
<Tab |
||||
key={`${t.value}-${index}`} |
||||
label={t.label} |
||||
active={t.value === currentTab} |
||||
onChangeTab={() => service.onCurrentTabChange(t.value!)} |
||||
/> |
||||
))} |
||||
</TabsBar> |
||||
} |
||||
> |
||||
{loading && <Spinner />} |
||||
{error && <Alert title={error.title}>{error.message}</Alert>} |
||||
|
||||
{currentTab === SnapshotTab.Data && ( |
||||
<div className={styles.code}> |
||||
<div className={styles.opts}> |
||||
<Field label="Template" className={styles.field}> |
||||
<Select options={options} value={showMessage} onChange={service.onShowMessageChange} /> |
||||
</Field> |
||||
|
||||
{showMessage === ShowMessage.GithubComment ? ( |
||||
<ClipboardButton icon="copy" getText={service.onGetMarkdownForClipboard}> |
||||
Copy to clipboard |
||||
</ClipboardButton> |
||||
) : ( |
||||
<Button icon="download-alt" onClick={service.onDownloadDashboard}> |
||||
Download ({snapshotSize}) |
||||
</Button> |
||||
)} |
||||
</div> |
||||
<AutoSizer disableWidth> |
||||
{({ height }) => ( |
||||
<CodeEditor |
||||
width="100%" |
||||
height={height} |
||||
language={showMessage === ShowMessage.GithubComment ? 'markdown' : 'json'} |
||||
showLineNumbers={true} |
||||
showMiniMap={true} |
||||
value={showMessage === ShowMessage.GithubComment ? markdownText : snapshotText} |
||||
readOnly={false} |
||||
onBlur={service.onSetSnapshotText} |
||||
/> |
||||
)} |
||||
</AutoSizer> |
||||
</div> |
||||
)} |
||||
{currentTab === SnapshotTab.Support && ( |
||||
<> |
||||
<Field |
||||
label="Randomize data" |
||||
description="Modify the original data to hide sensitve information. Note the lengths will stay the same, and duplicate values will be equal." |
||||
> |
||||
<HorizontalGroup> |
||||
<InlineSwitch |
||||
label="Labels" |
||||
id="randomize-labels" |
||||
showLabel={true} |
||||
value={Boolean(randomize.labels)} |
||||
onChange={() => service.onToggleRandomize('labels')} |
||||
/> |
||||
<InlineSwitch |
||||
label="Field names" |
||||
id="randomize-field-names" |
||||
showLabel={true} |
||||
value={Boolean(randomize.names)} |
||||
onChange={() => service.onToggleRandomize('names')} |
||||
/> |
||||
<InlineSwitch |
||||
label="String values" |
||||
id="randomize-string-values" |
||||
showLabel={true} |
||||
value={Boolean(randomize.values)} |
||||
onChange={() => service.onToggleRandomize('values')} |
||||
/> |
||||
</HorizontalGroup> |
||||
</Field> |
||||
|
||||
<Field label="Support snapshot" description={`Panel: ${panelTitle}`}> |
||||
<Stack> |
||||
<Button icon="download-alt" onClick={service.onDownloadDashboard}> |
||||
Dashboard ({snapshotSize}) |
||||
</Button> |
||||
<ClipboardButton |
||||
icon="github" |
||||
getText={service.onGetMarkdownForClipboard} |
||||
title="Copy a complete GitHub comment to the clipboard" |
||||
> |
||||
Copy to clipboard |
||||
</ClipboardButton> |
||||
</Stack> |
||||
</Field> |
||||
|
||||
<AutoSizer disableWidth> |
||||
{({ height }) => ( |
||||
<div style={{ height, overflow: 'auto' }}>{scene && <scene.Component model={scene} />}</div> |
||||
)} |
||||
</AutoSizer> |
||||
</> |
||||
)} |
||||
</Drawer> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => { |
||||
return { |
||||
code: css({ |
||||
flexGrow: 1, |
||||
height: '100%', |
||||
overflow: 'scroll', |
||||
}), |
||||
field: css({ |
||||
width: '100%', |
||||
}), |
||||
opts: css({ |
||||
display: 'flex', |
||||
width: '100%', |
||||
flexGrow: 0, |
||||
alignItems: 'center', |
||||
justifyContent: 'flex-end', |
||||
|
||||
'& button': { |
||||
marginLeft: theme.spacing(1), |
||||
}, |
||||
}), |
||||
}; |
||||
}; |
||||
@ -0,0 +1,140 @@ |
||||
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data'; |
||||
import { |
||||
SceneGridItem, |
||||
SceneGridLayout, |
||||
SceneQueryRunner, |
||||
SceneTimeRange, |
||||
VizPanel, |
||||
VizPanelMenu, |
||||
} from '@grafana/scenes'; |
||||
|
||||
import { DashboardScene } from '../../scene/DashboardScene'; |
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; |
||||
import { panelMenuBehavior } from '../../scene/PanelMenuBehavior'; |
||||
|
||||
import { SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; |
||||
|
||||
async function setup() { |
||||
const { panel } = await buildTestScene(); |
||||
|
||||
return new SupportSnapshotService(panel); |
||||
} |
||||
|
||||
describe('SupportSnapshotService', () => { |
||||
it('Can create it with default state', async () => { |
||||
const service = await setup(); |
||||
expect(service.state.currentTab).toBe(SnapshotTab.Support); |
||||
}); |
||||
|
||||
it('Can can build support snapshot dashboard', async () => { |
||||
const service = await setup(); |
||||
await service.buildDebugDashboard(); |
||||
expect(service.state.snapshot.panels[0].targets[0]).toMatchInlineSnapshot(` |
||||
{ |
||||
"datasource": { |
||||
"type": "grafana", |
||||
"uid": "grafana", |
||||
}, |
||||
"queryType": "snapshot", |
||||
"refId": "A", |
||||
"snapshot": [ |
||||
{ |
||||
"data": { |
||||
"values": [ |
||||
[ |
||||
1, |
||||
2, |
||||
3, |
||||
], |
||||
[ |
||||
11, |
||||
22, |
||||
33, |
||||
], |
||||
], |
||||
}, |
||||
"schema": { |
||||
"fields": [ |
||||
{ |
||||
"config": {}, |
||||
"name": "Time", |
||||
"type": "time", |
||||
}, |
||||
{ |
||||
"config": {}, |
||||
"name": "Value", |
||||
"type": "number", |
||||
}, |
||||
], |
||||
"meta": undefined, |
||||
"name": "http_requests_total", |
||||
"refId": undefined, |
||||
}, |
||||
}, |
||||
], |
||||
} |
||||
`);
|
||||
}); |
||||
}); |
||||
|
||||
async function buildTestScene() { |
||||
const menu = new VizPanelMenu({ |
||||
$behaviors: [panelMenuBehavior], |
||||
}); |
||||
|
||||
const panel = new VizPanel({ |
||||
title: 'Panel A', |
||||
pluginId: 'timeseries', |
||||
key: 'panel-12', |
||||
menu, |
||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], |
||||
$data: new SceneQueryRunner({ |
||||
data: { |
||||
state: LoadingState.Done, |
||||
series: [ |
||||
toDataFrame({ |
||||
name: 'http_requests_total', |
||||
fields: [ |
||||
{ name: 'Time', type: FieldType.time, values: [1, 2, 3] }, |
||||
{ name: 'Value', type: FieldType.number, values: [11, 22, 33] }, |
||||
], |
||||
}), |
||||
], |
||||
timeRange: getDefaultTimeRange(), |
||||
}, |
||||
datasource: { uid: 'my-uid' }, |
||||
queries: [{ query: 'QueryA', refId: 'A' }], |
||||
}), |
||||
}); |
||||
|
||||
const scene = new DashboardScene({ |
||||
title: 'My dashboard', |
||||
uid: 'dash-1', |
||||
tags: ['database', 'panel'], |
||||
$timeRange: new SceneTimeRange({ |
||||
from: 'now-5m', |
||||
to: 'now', |
||||
timeZone: 'Africa/Abidjan', |
||||
}), |
||||
meta: { |
||||
canEdit: true, |
||||
isEmbedded: false, |
||||
}, |
||||
body: new SceneGridLayout({ |
||||
children: [ |
||||
new SceneGridItem({ |
||||
key: 'griditem-1', |
||||
x: 0, |
||||
y: 0, |
||||
width: 10, |
||||
height: 12, |
||||
body: panel, |
||||
}), |
||||
], |
||||
}), |
||||
}); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
return { scene, panel, menu }; |
||||
} |
||||
@ -0,0 +1,132 @@ |
||||
import saveAs from 'file-saver'; |
||||
|
||||
import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; |
||||
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes'; |
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase'; |
||||
|
||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene'; |
||||
|
||||
import { Randomize } from './randomizer'; |
||||
import { getDebugDashboard, getGithubMarkdown } from './utils'; |
||||
|
||||
interface SupportSnapshotState { |
||||
currentTab: SnapshotTab; |
||||
showMessage: ShowMessage; |
||||
options: Array<SelectableValue<ShowMessage>>; |
||||
snapshotText: string; |
||||
markdownText: string; |
||||
snapshotSize?: string; |
||||
randomize: Randomize; |
||||
loading?: boolean; |
||||
error?: { |
||||
title: string; |
||||
message: string; |
||||
}; |
||||
panel: VizPanel; |
||||
panelTitle: string; |
||||
|
||||
// eslint-disable-next-line
|
||||
snapshot?: any; |
||||
snapshotUpdate: number; |
||||
scene?: SceneObject; |
||||
} |
||||
|
||||
export enum SnapshotTab { |
||||
Support, |
||||
Data, |
||||
} |
||||
|
||||
export enum ShowMessage { |
||||
PanelSnapshot, |
||||
GithubComment, |
||||
} |
||||
|
||||
export class SupportSnapshotService extends StateManagerBase<SupportSnapshotState> { |
||||
constructor(panel: VizPanel) { |
||||
super({ |
||||
panel, |
||||
panelTitle: sceneGraph.interpolate(panel, panel.state.title, {}, 'text'), |
||||
currentTab: SnapshotTab.Support, |
||||
showMessage: ShowMessage.GithubComment, |
||||
snapshotText: '', |
||||
markdownText: '', |
||||
randomize: {}, |
||||
snapshotUpdate: 0, |
||||
options: [ |
||||
{ |
||||
label: 'GitHub comment', |
||||
description: 'Copy and paste this message into a GitHub issue or comment', |
||||
value: ShowMessage.GithubComment, |
||||
}, |
||||
{ |
||||
label: 'Panel support snapshot', |
||||
description: 'Dashboard JSON used to help troubleshoot visualization issues', |
||||
value: ShowMessage.PanelSnapshot, |
||||
}, |
||||
], |
||||
}); |
||||
} |
||||
|
||||
async buildDebugDashboard() { |
||||
const { panel, randomize, snapshotUpdate } = this.state; |
||||
const snapshot = await getDebugDashboard(panel, randomize, sceneGraph.getTimeRange(panel).state.value); |
||||
const snapshotText = JSON.stringify(snapshot, null, 2); |
||||
const markdownText = getGithubMarkdown(panel, snapshotText); |
||||
const snapshotSize = formattedValueToString(getValueFormat('bytes')(snapshotText?.length ?? 0)); |
||||
|
||||
let scene: SceneObject | undefined = undefined; |
||||
|
||||
try { |
||||
const dash = transformSaveModelToScene({ dashboard: snapshot, meta: { isEmbedded: true } }); |
||||
scene = dash.state.body; // skip the wrappers
|
||||
} catch (ex) { |
||||
console.log('Error creating scene:', ex); |
||||
} |
||||
|
||||
this.setState({ snapshot, snapshotText, markdownText, snapshotSize, snapshotUpdate: snapshotUpdate + 1, scene }); |
||||
} |
||||
|
||||
onCurrentTabChange = (value: SnapshotTab) => { |
||||
this.setState({ currentTab: value }); |
||||
}; |
||||
|
||||
onShowMessageChange = (value: SelectableValue<ShowMessage>) => { |
||||
this.setState({ showMessage: value.value! }); |
||||
}; |
||||
|
||||
onGetMarkdownForClipboard = () => { |
||||
const { markdownText } = this.state; |
||||
const maxLen = Math.pow(1024, 2) * 1.5; // 1.5MB
|
||||
|
||||
if (markdownText.length > maxLen) { |
||||
this.setState({ |
||||
error: { |
||||
title: 'Copy to clipboard failed', |
||||
message: 'Snapshot is too large, consider download and attaching a file instead', |
||||
}, |
||||
}); |
||||
|
||||
return ''; |
||||
} |
||||
|
||||
return markdownText; |
||||
}; |
||||
|
||||
onDownloadDashboard = () => { |
||||
const { snapshotText, panelTitle } = this.state; |
||||
const blob = new Blob([snapshotText], { |
||||
type: 'text/plain', |
||||
}); |
||||
const fileName = `debug-${panelTitle}-${dateTimeFormat(new Date())}.json.txt`; |
||||
saveAs(blob, fileName); |
||||
}; |
||||
|
||||
onSetSnapshotText = (snapshotText: string) => { |
||||
this.setState({ snapshotText }); |
||||
}; |
||||
|
||||
onToggleRandomize = (k: keyof Randomize) => { |
||||
const { randomize } = this.state; |
||||
this.setState({ randomize: { ...randomize, [k]: !randomize[k] } }); |
||||
}; |
||||
} |
||||
@ -0,0 +1,307 @@ |
||||
import { cloneDeep } from 'lodash'; |
||||
|
||||
import { |
||||
dateTimeFormat, |
||||
TimeRange, |
||||
PanelData, |
||||
DataTransformerConfig, |
||||
DataFrameJSON, |
||||
LoadingState, |
||||
dataFrameToJSON, |
||||
DataTopic, |
||||
} from '@grafana/data'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { SceneGridItem, VizPanel } from '@grafana/scenes'; |
||||
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; |
||||
|
||||
import { gridItemToPanel } from '../../serialization/transformSceneToSaveModel'; |
||||
import { getQueryRunnerFor } from '../../utils/utils'; |
||||
|
||||
import { Randomize, randomizeData } from './randomizer'; |
||||
|
||||
export function getPanelDataFrames(data?: PanelData): DataFrameJSON[] { |
||||
const frames: DataFrameJSON[] = []; |
||||
if (data?.series) { |
||||
for (const f of data.series) { |
||||
frames.push(dataFrameToJSON(f)); |
||||
} |
||||
} |
||||
if (data?.annotations) { |
||||
for (const f of data.annotations) { |
||||
const json = dataFrameToJSON(f); |
||||
if (!json.schema?.meta) { |
||||
json.schema!.meta = {}; |
||||
} |
||||
json.schema!.meta.dataTopic = DataTopic.Annotations; |
||||
frames.push(json); |
||||
} |
||||
} |
||||
return frames; |
||||
} |
||||
|
||||
export function getGithubMarkdown(panel: VizPanel, snapshot: string): string { |
||||
const info = { |
||||
panelType: panel.state.pluginId, |
||||
datasource: '??', |
||||
}; |
||||
const grafanaVersion = `${config.buildInfo.version} (${config.buildInfo.commit})`; |
||||
|
||||
let md = `| Key | Value |
|
||||
|--|--| |
||||
| Panel | ${info.panelType} @ ${panel.state.pluginVersion ?? grafanaVersion} | |
||||
| Grafana | ${grafanaVersion} // ${config.buildInfo.edition} |
|
||||
`;
|
||||
|
||||
if (snapshot) { |
||||
md += '<details><summary>Panel debug snapshot dashboard</summary>\n\n```json\n' + snapshot + '\n```\n</details>'; |
||||
} |
||||
return md; |
||||
} |
||||
|
||||
export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRange: TimeRange) { |
||||
const saveModel = gridItemToPanel(panel.parent as SceneGridItem); |
||||
const dashboard = cloneDeep(embeddedDataTemplate); |
||||
const info = { |
||||
panelType: saveModel.type, |
||||
datasource: '??', |
||||
}; |
||||
|
||||
// reproducable
|
||||
const queryRunner = getQueryRunnerFor(panel)!; |
||||
|
||||
if (!queryRunner.state.data) { |
||||
return; |
||||
} |
||||
|
||||
const data = queryRunner.state.data; |
||||
|
||||
const dsref = queryRunner?.state.datasource; |
||||
const frames = randomizeData(getPanelDataFrames(data), rand); |
||||
const grafanaVersion = `${config.buildInfo.version} (${config.buildInfo.commit})`; |
||||
const queries = queryRunner.state.queries ?? []; |
||||
const html = `<table width="100%">
|
||||
<tr> |
||||
<th width="2%">Panel</th> |
||||
<td >${info.panelType} @ ${saveModel.pluginVersion ?? grafanaVersion}</td> |
||||
</tr> |
||||
<tr> |
||||
<th>Queries</th> |
||||
<td>${queries |
||||
.map((t) => { |
||||
const ds = t.datasource ?? dsref; |
||||
return `${t.refId}[${ds?.type}]`; |
||||
}) |
||||
.join(', ')}</td> |
||||
</tr> |
||||
${getTransformsRow(saveModel)} |
||||
${getDataRow(data, frames)} |
||||
${getAnnotationsRow(data)} |
||||
<tr> |
||||
<th>Grafana</th> |
||||
<td>${grafanaVersion} // ${config.buildInfo.edition}</td>
|
||||
</tr> |
||||
</table>`.trim();
|
||||
|
||||
// Replace the panel with embedded data
|
||||
dashboard.panels[0] = { |
||||
...saveModel, |
||||
...dashboard.panels[0], |
||||
targets: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
type: 'grafana', |
||||
uid: 'grafana', |
||||
}, |
||||
queryType: GrafanaQueryType.Snapshot, |
||||
snapshot: frames, |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
if (saveModel.transformations?.length) { |
||||
const last = dashboard.panels[dashboard.panels.length - 1]; |
||||
last.title = last.title + ' (after transformations)'; |
||||
|
||||
const before = cloneDeep(last); |
||||
before.id = 100; |
||||
before.title = 'Data (before transformations)'; |
||||
before.gridPos.w = 24; // full width
|
||||
before.targets[0].withTransforms = false; |
||||
dashboard.panels.push(before); |
||||
} |
||||
|
||||
if (data.annotations?.length) { |
||||
dashboard.panels.push({ |
||||
id: 7, |
||||
gridPos: { |
||||
h: 6, |
||||
w: 24, |
||||
x: 0, |
||||
y: 20, |
||||
}, |
||||
type: 'table', |
||||
title: 'Annotations', |
||||
datasource: { |
||||
type: 'datasource', |
||||
uid: '-- Dashboard --', |
||||
}, |
||||
options: { |
||||
showTypeIcons: true, |
||||
}, |
||||
targets: [ |
||||
{ |
||||
datasource: { |
||||
type: 'datasource', |
||||
uid: '-- Dashboard --', |
||||
}, |
||||
panelId: 2, |
||||
withTransforms: true, |
||||
topic: DataTopic.Annotations, |
||||
refId: 'A', |
||||
}, |
||||
], |
||||
}); |
||||
} |
||||
|
||||
dashboard.panels[1].options.content = html; |
||||
dashboard.panels[2].options.content = JSON.stringify(saveModel, null, 2); |
||||
|
||||
dashboard.title = `Debug: ${saveModel.title} // ${dateTimeFormat(new Date())}`; |
||||
dashboard.tags = ['debug', `debug-${info.panelType}`]; |
||||
dashboard.time = { |
||||
from: timeRange.from.toISOString(), |
||||
to: timeRange.to.toISOString(), |
||||
}; |
||||
|
||||
return dashboard; |
||||
} |
||||
|
||||
// eslint-disable-next-line
|
||||
function getTransformsRow(saveModel: any): string { |
||||
if (!saveModel.transformations) { |
||||
return ''; |
||||
} |
||||
return `<tr>
|
||||
<th>Transform</th> |
||||
<td>${saveModel.transformations.map((t: DataTransformerConfig) => t.id).join(', ')}</td> |
||||
</tr>`;
|
||||
} |
||||
|
||||
function getDataRow(data: PanelData, frames: DataFrameJSON[]): string { |
||||
let frameCount = data.series.length ?? 0; |
||||
let fieldCount = 0; |
||||
let rowCount = 0; |
||||
for (const frame of data.series) { |
||||
fieldCount += frame.fields.length; |
||||
rowCount += frame.length; |
||||
} |
||||
return ( |
||||
'<tr>' + |
||||
'<th>Data</th>' + |
||||
'<td>' + |
||||
`${data.state !== LoadingState.Done ? data.state : ''} ` + |
||||
`${frameCount} frames, ${fieldCount} fields, ` + |
||||
`${rowCount} rows ` + |
||||
// `(${formattedValueToString(getValueFormat('decbytes')(raw?.length))} JSON)` +
|
||||
'</td>' + |
||||
'</tr>' |
||||
); |
||||
} |
||||
|
||||
function getAnnotationsRow(data: PanelData): string { |
||||
if (!data.annotations?.length) { |
||||
return ''; |
||||
} |
||||
|
||||
return `<tr>
|
||||
<th>Annotations</th> |
||||
<td>${data.annotations.map((a, idx) => `<span>${a.length}</span>`)}</td> |
||||
</tr>`;
|
||||
} |
||||
|
||||
// eslint-disable-next-line
|
||||
const embeddedDataTemplate: any = { |
||||
// should be dashboard model when that is accurate enough
|
||||
panels: [ |
||||
{ |
||||
id: 2, |
||||
title: 'Reproduced with embedded data', |
||||
datasource: { |
||||
type: 'grafana', |
||||
uid: 'grafana', |
||||
}, |
||||
gridPos: { |
||||
h: 13, |
||||
w: 15, |
||||
x: 0, |
||||
y: 0, |
||||
}, |
||||
}, |
||||
{ |
||||
gridPos: { |
||||
h: 7, |
||||
w: 9, |
||||
x: 15, |
||||
y: 0, |
||||
}, |
||||
id: 5, |
||||
options: { |
||||
content: '...', |
||||
mode: 'html', |
||||
}, |
||||
title: 'Debug info', |
||||
type: 'text', |
||||
}, |
||||
{ |
||||
id: 6, |
||||
title: 'Original Panel JSON', |
||||
type: 'text', |
||||
gridPos: { |
||||
h: 13, |
||||
w: 9, |
||||
x: 15, |
||||
y: 7, |
||||
}, |
||||
options: { |
||||
content: '...', |
||||
mode: 'code', |
||||
code: { |
||||
language: 'json', |
||||
showLineNumbers: true, |
||||
showMiniMap: true, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
id: 3, |
||||
title: 'Data from panel above', |
||||
type: 'table', |
||||
datasource: { |
||||
type: 'datasource', |
||||
uid: '-- Dashboard --', |
||||
}, |
||||
gridPos: { |
||||
h: 7, |
||||
w: 15, |
||||
x: 0, |
||||
y: 13, |
||||
}, |
||||
options: { |
||||
showTypeIcons: true, |
||||
}, |
||||
targets: [ |
||||
{ |
||||
datasource: { |
||||
type: 'datasource', |
||||
uid: '-- Dashboard --', |
||||
}, |
||||
panelId: 2, |
||||
withTransforms: true, |
||||
refId: 'A', |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
schemaVersion: 37, |
||||
}; |
||||
Loading…
Reference in new issue