mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SupportSnapshots: improved iframe support (#55059)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
@@ -99,6 +99,9 @@ export class GrafanaApp {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
|
// Let iframe container know grafana has started loading
|
||||||
|
parent.postMessage('GrafanaAppInit', '*');
|
||||||
|
|
||||||
setBackendSrv(backendSrv);
|
setBackendSrv(backendSrv);
|
||||||
initEchoSrv();
|
initEchoSrv();
|
||||||
addClassIfNoOverlayScrollbar();
|
addClassIfNoOverlayScrollbar();
|
||||||
|
|||||||
41
public/app/core/services/StateManagerBase.ts
Normal file
41
public/app/core/services/StateManagerBase.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { useForceUpdate } from '@grafana/ui';
|
||||||
|
|
||||||
|
export class StateManagerBase<TState> {
|
||||||
|
subject = new Subject<TState>();
|
||||||
|
state: TState;
|
||||||
|
|
||||||
|
constructor(state: TState) {
|
||||||
|
this.state = state;
|
||||||
|
this.subject.next(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
useState() {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
return useLatestState(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(update: Partial<TState>) {
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
...update,
|
||||||
|
};
|
||||||
|
this.subject.next(this.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This hook is always returning model.state instead of a useState that remembers the last state emitted on the subject
|
||||||
|
* The reason for this is so that if the model instance change this function will always return the latest state.
|
||||||
|
*/
|
||||||
|
function useLatestState<TState>(model: StateManagerBase<TState>): TState {
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const s = model.subject.subscribe(forceUpdate);
|
||||||
|
return () => s.unsubscribe();
|
||||||
|
}, [model, forceUpdate]);
|
||||||
|
|
||||||
|
return model.state;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data';
|
||||||
|
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||||
|
|
||||||
|
import { PanelModel } from '../../state/PanelModel';
|
||||||
|
|
||||||
|
import { SupportSnapshot } from './SupportSnapshot';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const panel = new PanelModel({});
|
||||||
|
panel.plugin = getPanelPlugin({});
|
||||||
|
panel.getQueryRunner().setLastResult({
|
||||||
|
timeRange: getDefaultTimeRange(),
|
||||||
|
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] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
panel.getQueryRunner().resendLastResult();
|
||||||
|
|
||||||
|
return render(<SupportSnapshot panel={panel} onClose={() => {}} plugin={panel.plugin} />);
|
||||||
|
}
|
||||||
|
describe('SupportSnapshot', () => {
|
||||||
|
it('Can render', async () => {
|
||||||
|
setup();
|
||||||
|
expect(await screen.findByRole('button', { name: 'Dashboard (2.94 KiB)' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { saveAs } from 'file-saver';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import React, { useState, useMemo } from 'react';
|
|
||||||
import { useAsync, useCopyToClipboard } from 'react-use';
|
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import {
|
import { PanelPlugin, GrafanaTheme2, FeatureState } from '@grafana/data';
|
||||||
PanelPlugin,
|
import { config } from '@grafana/runtime';
|
||||||
GrafanaTheme2,
|
|
||||||
AppEvents,
|
|
||||||
SelectableValue,
|
|
||||||
dateTimeFormat,
|
|
||||||
getValueFormat,
|
|
||||||
FeatureState,
|
|
||||||
formattedValueToString,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { config, getTemplateSrv } from '@grafana/runtime';
|
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
Tab,
|
Tab,
|
||||||
@@ -29,16 +18,13 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
FeatureBadge,
|
FeatureBadge,
|
||||||
Select,
|
Select,
|
||||||
|
ClipboardButton,
|
||||||
|
Stack,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import appEvents from 'app/core/app_events';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { contextSrv } from 'app/core/core';
|
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { setDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
|
|
||||||
import { InspectTab } from 'app/features/inspector/types';
|
|
||||||
|
|
||||||
import { Randomize } from './randomizer';
|
import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService';
|
||||||
import { getGithubMarkdown, getDebugDashboard } from './utils';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -46,145 +32,90 @@ interface Props {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ShowMessge {
|
|
||||||
PanelSnapshot = 'snap',
|
|
||||||
GithubComment = 'github',
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: Array<SelectableValue<ShowMessge>> = [
|
|
||||||
{
|
|
||||||
label: 'Github comment',
|
|
||||||
description: 'Copy and paste this message into a github issue or comment',
|
|
||||||
value: ShowMessge.GithubComment,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Panel support snapshot',
|
|
||||||
description: 'Dashboard JSON used to help troubleshoot visualization issues',
|
|
||||||
value: ShowMessge.PanelSnapshot,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SupportSnapshot({ panel, plugin, onClose }: Props) {
|
export function SupportSnapshot({ panel, plugin, onClose }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [currentTab, setCurrentTab] = useState(InspectTab.Support);
|
const service = useMemo(() => new SupportSnapshotService(panel), [panel]);
|
||||||
const [showMessage, setShowMessge] = useState(ShowMessge.GithubComment);
|
|
||||||
const [snapshotText, setDashboardText] = useState('...');
|
|
||||||
const [rand, setRand] = useState<Randomize>({});
|
|
||||||
const [_, copyToClipboard] = useCopyToClipboard();
|
|
||||||
const info = useAsync(async () => {
|
|
||||||
const dashboard = await getDebugDashboard(panel, rand, getTimeSrv().timeRange());
|
|
||||||
setDashboardToFetchFromLocalStorage({ meta: {}, dashboard });
|
|
||||||
setDashboardText(JSON.stringify(dashboard, null, 2));
|
|
||||||
}, [rand, panel, plugin, setDashboardText, currentTab]);
|
|
||||||
|
|
||||||
const snapshotSize = useMemo(() => {
|
const {
|
||||||
return formattedValueToString(getValueFormat('bytes')(snapshotText?.length ?? 0));
|
currentTab,
|
||||||
}, [snapshotText]);
|
loading,
|
||||||
|
error,
|
||||||
|
iframeLoading,
|
||||||
|
options,
|
||||||
|
showMessage,
|
||||||
|
snapshotSize,
|
||||||
|
markdownText,
|
||||||
|
snapshotText,
|
||||||
|
randomize,
|
||||||
|
panelTitle,
|
||||||
|
snapshotUpdate,
|
||||||
|
} = service.useState();
|
||||||
|
|
||||||
const markdownText = useMemo(() => {
|
useEffect(() => {
|
||||||
return getGithubMarkdown(panel, snapshotText);
|
service.buildDebugDashboard();
|
||||||
}, [snapshotText, panel]);
|
}, [service, plugin, randomize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Listen for messages from loaded iframe
|
||||||
|
return service.subscribeToIframeLoadingMessage();
|
||||||
|
}, [service]);
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelTitle = getTemplateSrv().replace(panel.title, panel.scopedVars, 'text') || 'Panel';
|
|
||||||
|
|
||||||
const toggleRandomize = (k: keyof Randomize) => {
|
|
||||||
setRand({ ...rand, [k]: !rand[k] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const doImportDashboard = () => {
|
|
||||||
setDashboardToFetchFromLocalStorage({ meta: {}, dashboard: JSON.parse(snapshotText) });
|
|
||||||
global.open(config.appUrl + 'dashboard/new', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const doDownloadDashboard = () => {
|
|
||||||
const blob = new Blob([snapshotText], {
|
|
||||||
type: 'text/plain',
|
|
||||||
});
|
|
||||||
const fileName = `debug-${panelTitle}-${dateTimeFormat(new Date())}.json.txt`;
|
|
||||||
saveAs(blob, fileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doCopyMarkdown = () => {
|
|
||||||
const maxLen = Math.pow(1024, 2) * 1.5; // 1.5MB
|
|
||||||
if (markdownText.length > maxLen) {
|
|
||||||
appEvents.emit(AppEvents.alertError, [
|
|
||||||
`Snapshot is too large`,
|
|
||||||
'Consider downloading and attaching the file instead',
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyToClipboard(markdownText);
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, [`Message copied`]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: 'Support', value: InspectTab.Support },
|
{ label: 'Support', value: SnapshotTab.Support },
|
||||||
{ label: 'Data', value: InspectTab.JSON },
|
{ label: 'Data', value: SnapshotTab.Data },
|
||||||
];
|
];
|
||||||
let activeTab = currentTab;
|
|
||||||
if (!tabs.find((item) => item.value === currentTab)) {
|
|
||||||
activeTab = InspectTab.JSON;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderError = () => {
|
|
||||||
console.error('Error', info.error);
|
|
||||||
return <Alert title="Error loading dashboard">{`${info.error}`}</Alert>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={`Panel: ${panelTitle}`}
|
title={`Support snapshot`}
|
||||||
width="90%"
|
width="90%"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
expandable
|
expandable
|
||||||
scrollableContent
|
scrollableContent
|
||||||
subtitle={
|
subtitle={
|
||||||
<div>
|
<Stack direction="column" gap={1}>
|
||||||
<p>
|
<span>
|
||||||
<FeatureBadge featureState={FeatureState.beta} />
|
<FeatureBadge featureState={FeatureState.beta} />
|
||||||
</p>
|
</span>
|
||||||
</div>
|
<span className="muted">
|
||||||
|
A support snapshot contains the query response data and raw panel settings. Include this snapshot in support
|
||||||
|
requests to help identify issues faster
|
||||||
|
</span>
|
||||||
|
</Stack>
|
||||||
}
|
}
|
||||||
tabs={
|
tabs={
|
||||||
<TabsBar>
|
<TabsBar>
|
||||||
{tabs.map((t, index) => {
|
{tabs.map((t, index) => (
|
||||||
return (
|
<Tab
|
||||||
<Tab
|
key={`${t.value}-${index}`}
|
||||||
key={`${t.value}-${index}`}
|
label={t.label}
|
||||||
label={t.label}
|
active={t.value === currentTab}
|
||||||
active={t.value === activeTab}
|
onChangeTab={() => service.onCurrentTabChange(t.value!)}
|
||||||
onChangeTab={() => setCurrentTab(t.value || InspectTab.Support)}
|
/>
|
||||||
/>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabsBar>
|
</TabsBar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{info.loading && <Spinner />}
|
{loading && <Spinner />}
|
||||||
{info.error && renderError()}
|
{error && <Alert title={error.title}>{error.message}</Alert>}
|
||||||
|
|
||||||
{activeTab === InspectTab.JSON ? (
|
{currentTab === SnapshotTab.Data && (
|
||||||
<div className={styles.code}>
|
<div className={styles.code}>
|
||||||
<div className={styles.opts}>
|
<div className={styles.opts}>
|
||||||
<Field label="Template" className={styles.field}>
|
<Field label="Template" className={styles.field}>
|
||||||
<Select
|
<Select options={options} value={showMessage} onChange={service.onShowMessageChange} />
|
||||||
options={options}
|
|
||||||
value={options.find((v) => v.value === showMessage) ?? options[0]}
|
|
||||||
onChange={(v) => setShowMessge(v.value ?? options[0].value!)}
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{showMessage === ShowMessge.GithubComment ? (
|
{showMessage === ShowMessage.GithubComment ? (
|
||||||
<Button icon="github" onClick={doCopyMarkdown}>
|
<ClipboardButton icon="copy" getText={service.onGetMarkdownForClipboard}>
|
||||||
Copy
|
Copy to clipboard
|
||||||
</Button>
|
</ClipboardButton>
|
||||||
) : (
|
) : (
|
||||||
<Button icon="download-alt" onClick={doDownloadDashboard}>
|
<Button icon="download-alt" onClick={service.onDownloadDashboard}>
|
||||||
Download ({snapshotSize})
|
Download ({snapshotSize})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -194,73 +125,85 @@ export function SupportSnapshot({ panel, plugin, onClose }: Props) {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
width="100%"
|
width="100%"
|
||||||
height={height}
|
height={height}
|
||||||
language={showMessage === ShowMessge.GithubComment ? 'markdown' : 'json'}
|
language={showMessage === ShowMessage.GithubComment ? 'markdown' : 'json'}
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
showMiniMap={true}
|
showMiniMap={true}
|
||||||
value={showMessage === ShowMessge.GithubComment ? markdownText : snapshotText}
|
value={showMessage === ShowMessage.GithubComment ? markdownText : snapshotText}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
onBlur={setDashboardText}
|
onBlur={service.onSetSnapshotText}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
|
{currentTab === SnapshotTab.Support && (
|
||||||
<>
|
<>
|
||||||
{false && (
|
|
||||||
<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"
|
|
||||||
showLabel={true}
|
|
||||||
value={Boolean(rand.labels)}
|
|
||||||
onChange={(v) => toggleRandomize('labels')}
|
|
||||||
/>
|
|
||||||
{/* <InlineSwitch
|
|
||||||
label="Field names"
|
|
||||||
showLabel={true}
|
|
||||||
value={Boolean(rand.names)}
|
|
||||||
onChange={(v) => toggleRandomize('names')}
|
|
||||||
/>
|
|
||||||
<InlineSwitch
|
|
||||||
label="String values"
|
|
||||||
showLabel={true}
|
|
||||||
value={Boolean(rand.values)}
|
|
||||||
onChange={(v) => toggleRandomize('values')}
|
|
||||||
/> */}
|
|
||||||
</HorizontalGroup>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label="Support snapshot"
|
label="Randomize data"
|
||||||
description="This snapshot contains the query response data and raw panel settings. Including this snapshot in support requests can help identify issues faster."
|
description="Modify the original data to hide sensitve information. Note the lengths will stay the same, and duplicate values will be equal."
|
||||||
>
|
>
|
||||||
<>
|
<HorizontalGroup>
|
||||||
<HorizontalGroup>
|
<InlineSwitch
|
||||||
<Button icon="download-alt" onClick={doDownloadDashboard}>
|
label="Labels"
|
||||||
Dashboard ({snapshotSize})
|
id="randomize-labels"
|
||||||
</Button>
|
showLabel={true}
|
||||||
<Button icon="github" onClick={doCopyMarkdown} title="Paste this into a github issue">
|
value={Boolean(randomize.labels)}
|
||||||
Copy for github
|
onChange={() => service.onToggleRandomize('labels')}
|
||||||
</Button>
|
/>
|
||||||
<Button onClick={doImportDashboard} variant="secondary">
|
<InlineSwitch
|
||||||
Preview
|
label="Field names"
|
||||||
</Button>
|
id="randomize-field-names"
|
||||||
</HorizontalGroup>
|
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>
|
||||||
|
<Button
|
||||||
|
onClick={service.onPreviewDashboard}
|
||||||
|
variant="secondary"
|
||||||
|
title="Open support snapshot dashboard in a new tab"
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<AutoSizer disableWidth>
|
<AutoSizer disableWidth>
|
||||||
{({ height }) => (
|
{({ height }) => (
|
||||||
<iframe
|
<>
|
||||||
src={`/dashboard/new?orgId=${contextSrv.user.orgId}&kiosk`}
|
<iframe
|
||||||
width="100%"
|
src={`${config.appUrl}dashboard/new?orgId=${contextSrv.user.orgId}&kiosk&${snapshotUpdate}`}
|
||||||
height={height - 100}
|
width="100%"
|
||||||
frameBorder="0"
|
height={height - 100}
|
||||||
/>
|
frameBorder="0"
|
||||||
|
style={{
|
||||||
|
display: iframeLoading ? 'block' : 'none',
|
||||||
|
marginTop: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!iframeLoading && <div> </div>}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
|
import { PanelModel } from '../../state/PanelModel';
|
||||||
|
|
||||||
|
import { SnapshotTab, SupportSnapshotService } from './SupportSnapshotService';
|
||||||
|
|
||||||
|
describe('SupportSnapshotService', () => {
|
||||||
|
const panel = new PanelModel({});
|
||||||
|
panel.getQueryRunner().setLastResult({
|
||||||
|
timeRange: getDefaultTimeRange(),
|
||||||
|
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] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
panel.getQueryRunner().resendLastResult();
|
||||||
|
|
||||||
|
it('Can create it with default state', () => {
|
||||||
|
const service = new SupportSnapshotService(panel);
|
||||||
|
expect(service.state.currentTab).toBe(SnapshotTab.Support);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can can build support snapshot dashboard', async () => {
|
||||||
|
const service = new SupportSnapshotService(panel);
|
||||||
|
await service.buildDebugDashboard();
|
||||||
|
expect(service.state.snapshot.panels[0].targets[0]).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"datasource": Object {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "grafana",
|
||||||
|
},
|
||||||
|
"queryType": "snapshot",
|
||||||
|
"refId": "A",
|
||||||
|
"snapshot": Array [
|
||||||
|
Object {
|
||||||
|
"data": Object {
|
||||||
|
"values": Array [
|
||||||
|
Array [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
11,
|
||||||
|
22,
|
||||||
|
33,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"schema": Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"name": "Time",
|
||||||
|
"type": "time",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"name": "Value",
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"meta": undefined,
|
||||||
|
"name": "http_requests_total",
|
||||||
|
"refId": undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import saveAs from 'file-saver';
|
||||||
|
|
||||||
|
import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||||
|
|
||||||
|
import { getTimeSrv } from '../../services/TimeSrv';
|
||||||
|
import { PanelModel } from '../../state';
|
||||||
|
import { setDashboardToFetchFromLocalStorage } from '../../state/initDashboard';
|
||||||
|
|
||||||
|
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;
|
||||||
|
iframeLoading?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
error?: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
panel: PanelModel;
|
||||||
|
panelTitle: string;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
snapshot?: any;
|
||||||
|
snapshotUpdate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SnapshotTab {
|
||||||
|
Support,
|
||||||
|
Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ShowMessage {
|
||||||
|
PanelSnapshot,
|
||||||
|
GithubComment,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SupportSnapshotService extends StateManagerBase<SupportSnapshotState> {
|
||||||
|
constructor(panel: PanelModel) {
|
||||||
|
super({
|
||||||
|
panel,
|
||||||
|
panelTitle: panel.replaceVariables(panel.title, undefined, 'text') || 'Panel',
|
||||||
|
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, iframeLoading, currentTab } = this.state;
|
||||||
|
const snapshot = await getDebugDashboard(panel, randomize, getTimeSrv().timeRange());
|
||||||
|
const snapshotText = JSON.stringify(snapshot, null, 2);
|
||||||
|
const markdownText = getGithubMarkdown(panel, snapshotText);
|
||||||
|
const snapshotSize = formattedValueToString(getValueFormat('bytes')(snapshotText?.length ?? 0));
|
||||||
|
|
||||||
|
if (iframeLoading && currentTab === SnapshotTab.Support) {
|
||||||
|
setDashboardToFetchFromLocalStorage({ meta: {}, dashboard: snapshot });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ snapshot, snapshotText, markdownText, snapshotSize, snapshotUpdate: snapshotUpdate + 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
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] } });
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviewDashboard = () => {
|
||||||
|
const { snapshot } = this.state;
|
||||||
|
if (snapshot) {
|
||||||
|
setDashboardToFetchFromLocalStorage({ meta: {}, dashboard: snapshot });
|
||||||
|
global.open(config.appUrl + 'dashboard/new', '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribeToIframeLoadingMessage() {
|
||||||
|
const handleEvent = (evt: MessageEvent<string>) => {
|
||||||
|
if (evt.data === 'GrafanaAppInit') {
|
||||||
|
setDashboardToFetchFromLocalStorage({ meta: {}, dashboard: this.state.snapshot });
|
||||||
|
this.setState({ iframeLoading: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', handleEvent, false);
|
||||||
|
|
||||||
|
return function cleanup() {
|
||||||
|
window.removeEventListener('message', handleEvent);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ export async function getDebugDashboard(panel: PanelModel, rand: Randomize, time
|
|||||||
withTransforms: false,
|
withTransforms: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const dsref = panel.datasource;
|
const dsref = panel.datasource;
|
||||||
const frames = randomizeData(getPanelDataFrames(data), rand);
|
const frames = randomizeData(getPanelDataFrames(data), rand);
|
||||||
const grafanaVersion = `${config.buildInfo.version} (${config.buildInfo.commit})`;
|
const grafanaVersion = `${config.buildInfo.version} (${config.buildInfo.commit})`;
|
||||||
|
|||||||
@@ -351,6 +351,11 @@ export class PanelQueryRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Useful from tests */
|
||||||
|
setLastResult(data: PanelData) {
|
||||||
|
this.lastResult = data;
|
||||||
|
}
|
||||||
|
|
||||||
getLastResult(): PanelData | undefined {
|
getLastResult(): PanelData | undefined {
|
||||||
return this.lastResult;
|
return this.lastResult;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user