diff --git a/public/app/app.ts b/public/app/app.ts index 2ae41005d67..3841fcd79e4 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -99,6 +99,9 @@ export class GrafanaApp { async init() { try { + // Let iframe container know grafana has started loading + parent.postMessage('GrafanaAppInit', '*'); + setBackendSrv(backendSrv); initEchoSrv(); addClassIfNoOverlayScrollbar(); diff --git a/public/app/core/services/StateManagerBase.ts b/public/app/core/services/StateManagerBase.ts new file mode 100644 index 00000000000..e749f2e529d --- /dev/null +++ b/public/app/core/services/StateManagerBase.ts @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { Subject } from 'rxjs'; + +import { useForceUpdate } from '@grafana/ui'; + +export class StateManagerBase { + subject = new Subject(); + 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) { + 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(model: StateManagerBase): TState { + const forceUpdate = useForceUpdate(); + + useEffect(() => { + const s = model.subject.subscribe(forceUpdate); + return () => s.unsubscribe(); + }, [model, forceUpdate]); + + return model.state; +} diff --git a/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.test.tsx b/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.test.tsx new file mode 100644 index 00000000000..74cf9e5d54c --- /dev/null +++ b/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.test.tsx @@ -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( {}} plugin={panel.plugin} />); +} +describe('SupportSnapshot', () => { + it('Can render', async () => { + setup(); + expect(await screen.findByRole('button', { name: 'Dashboard (2.94 KiB)' })).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.tsx b/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.tsx index 6297ee681dc..f59cb1458a3 100644 --- a/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.tsx +++ b/public/app/features/dashboard/components/SupportSnapshot/SupportSnapshot.tsx @@ -1,20 +1,9 @@ import { css } from '@emotion/css'; -import { saveAs } from 'file-saver'; -import React, { useState, useMemo } from 'react'; -import { useAsync, useCopyToClipboard } from 'react-use'; +import React, { useMemo, useEffect } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { - PanelPlugin, - GrafanaTheme2, - AppEvents, - SelectableValue, - dateTimeFormat, - getValueFormat, - FeatureState, - formattedValueToString, -} from '@grafana/data'; -import { config, getTemplateSrv } from '@grafana/runtime'; +import { PanelPlugin, GrafanaTheme2, FeatureState } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Drawer, Tab, @@ -29,16 +18,13 @@ import { Alert, FeatureBadge, Select, + ClipboardButton, + Stack, } from '@grafana/ui'; -import appEvents from 'app/core/app_events'; -import { contextSrv } from 'app/core/core'; -import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { contextSrv } from 'app/core/services/context_srv'; 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 { getGithubMarkdown, getDebugDashboard } from './utils'; +import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; interface Props { panel: PanelModel; @@ -46,145 +32,90 @@ interface Props { onClose: () => void; } -enum ShowMessge { - PanelSnapshot = 'snap', - GithubComment = 'github', -} - -const options: Array> = [ - { - 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) { const styles = useStyles2(getStyles); - const [currentTab, setCurrentTab] = useState(InspectTab.Support); - const [showMessage, setShowMessge] = useState(ShowMessge.GithubComment); - const [snapshotText, setDashboardText] = useState('...'); - const [rand, setRand] = useState({}); - 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 service = useMemo(() => new SupportSnapshotService(panel), [panel]); - const snapshotSize = useMemo(() => { - return formattedValueToString(getValueFormat('bytes')(snapshotText?.length ?? 0)); - }, [snapshotText]); + const { + currentTab, + loading, + error, + iframeLoading, + options, + showMessage, + snapshotSize, + markdownText, + snapshotText, + randomize, + panelTitle, + snapshotUpdate, + } = service.useState(); - const markdownText = useMemo(() => { - return getGithubMarkdown(panel, snapshotText); - }, [snapshotText, panel]); + useEffect(() => { + service.buildDebugDashboard(); + }, [service, plugin, randomize]); + + useEffect(() => { + // Listen for messages from loaded iframe + return service.subscribeToIframeLoadingMessage(); + }, [service]); if (!plugin) { 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 = [ - { label: 'Support', value: InspectTab.Support }, - { label: 'Data', value: InspectTab.JSON }, + { label: 'Support', value: SnapshotTab.Support }, + { 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 {`${info.error}`}; - }; return ( -

+ + -

- + + + A support snapshot contains the query response data and raw panel settings. Include this snapshot in support + requests to help identify issues faster + + } tabs={ - {tabs.map((t, index) => { - return ( - setCurrentTab(t.value || InspectTab.Support)} - /> - ); - })} + {tabs.map((t, index) => ( + service.onCurrentTabChange(t.value!)} + /> + ))} } > - {info.loading && } - {info.error && renderError()} + {loading && } + {error && {error.message}} - {activeTab === InspectTab.JSON ? ( + {currentTab === SnapshotTab.Data && (
- - {showMessage === ShowMessge.GithubComment ? ( - + {showMessage === ShowMessage.GithubComment ? ( + + Copy to clipboard + ) : ( - )} @@ -194,73 +125,85 @@ export function SupportSnapshot({ panel, plugin, onClose }: Props) { )}
- ) : ( + )} + {currentTab === SnapshotTab.Support && ( <> - {false && ( - - - toggleRandomize('labels')} - /> - {/* toggleRandomize('names')} - /> - toggleRandomize('values')} - /> */} - - - )} - - <> - - - - - - + + service.onToggleRandomize('labels')} + /> + service.onToggleRandomize('names')} + /> + service.onToggleRandomize('values')} + /> + + + + + + + + Copy to clipboard + + + {({ height }) => ( -