diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index bee1674a8ca..d62895b2088 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -13,6 +13,7 @@ export interface BuildInfo { interface FeatureToggles { transformations: boolean; + inspect: boolean; expressions: boolean; } export class GrafanaBootConfig { @@ -48,6 +49,7 @@ export class GrafanaBootConfig { pluginsToPreload: string[] = []; featureToggles: FeatureToggles = { transformations: false, + inspect: false, expressions: false, }; diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 01e8329625d..5c144ab02e2 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -13,6 +13,7 @@ import 'mousetrap-global-bind'; import { ContextSrv } from './context_srv'; import { ILocationService, ITimeoutService, IRootScopeService } from 'angular'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; +import { getLocationSrv } from '@grafana/runtime'; export class KeybindingSrv { helpModal: boolean; @@ -264,6 +265,13 @@ export class KeybindingSrv { } }); + // inspect panel + this.bind('p i', () => { + if (dashboard.meta.focusPanelId) { + getLocationSrv().update({ partial: true, query: { inspect: dashboard.meta.focusPanelId } }); + } + }); + // toggle panel legend this.bind('p l', () => { if (dashboard.meta.focusPanelId) { diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx new file mode 100644 index 00000000000..23549b76c3c --- /dev/null +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -0,0 +1,58 @@ +// Libraries +import React, { PureComponent } from 'react'; + +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; +import { JSONFormatter, Modal } from '@grafana/ui'; +import { css } from 'emotion'; +import { getLocationSrv } from '@grafana/runtime'; + +interface Props { + dashboard: DashboardModel; + panel: PanelModel; +} + +interface State {} + +export class PanelInspector extends PureComponent { + constructor(props: Props) { + super(props); + } + + onDismiss = () => { + getLocationSrv().update({ + query: { inspect: null }, + partial: true, + }); + }; + + render() { + const { panel } = this.props; + if (!panel) { + this.onDismiss(); // Try to close the component + return null; + } + const bodyStyle = css` + max-height: 70vh; + overflow-y: scroll; + `; + + // TODO? should we get the result with an observable once? + const data = (panel.getQueryRunner() as any).lastResult; + return ( + + + {panel.title ? panel.title : 'Panel'} + + } + onDismiss={this.onDismiss} + isOpen={true} + > +
+ +
+
+ ); + } +} diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index b498e8d1747..e8df2ab25de 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -31,6 +31,7 @@ import { AppNotificationSeverity, } from 'app/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; +import { PanelInspector } from '../components/Inspector/PanelInspector'; export interface Props { urlUid?: string; urlSlug?: string; @@ -38,6 +39,7 @@ export interface Props { editview?: string; urlPanelId?: string; urlFolderId?: string; + inspectPanelId?: string; $scope: any; $injector: any; routeInfo: DashboardRouteInfo; @@ -250,7 +252,7 @@ export class DashboardPage extends PureComponent { } render() { - const { dashboard, editview, $injector, isInitSlow, initError } = this.props; + const { dashboard, editview, $injector, isInitSlow, initError, inspectPanelId } = this.props; const { isSettingsOpening, isEditing, isFullscreen, scrollTop, updateScrollTop } = this.state; if (!dashboard) { @@ -270,6 +272,9 @@ export class DashboardPage extends PureComponent { 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, }); + // Find the panel to inspect + const inspectPanel = inspectPanelId ? dashboard.getPanelById(parseInt(inspectPanelId, 10)) : null; + // Only trigger render when the scroll has moved by 25 const approximateScrollTop = Math.round(scrollTop / 25) * 25; @@ -306,6 +311,8 @@ export class DashboardPage extends PureComponent { + + {inspectPanel && } ); } @@ -320,6 +327,7 @@ export const mapStateToProps = (state: StoreState) => ({ urlFolderId: state.location.query.folderId, urlFullscreen: !!state.location.query.fullscreen, urlEdit: !!state.location.query.edit, + inspectPanelId: state.location.query.inspect, initPhase: state.dashboard.initPhase, isInitSlow: state.dashboard.isInitSlow, initError: state.dashboard.initError, diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index ce12700f92d..53e0ea72a57 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -1,9 +1,11 @@ import { updateLocation } from 'app/core/actions'; import { store } from 'app/store/store'; +import config from 'app/core/config'; import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +import { getLocationSrv } from '@grafana/runtime'; import { PanelMenuItem } from '@grafana/data'; export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { @@ -37,6 +39,15 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { sharePanel(dashboard, panel); }; + const onInspectPanel = () => { + getLocationSrv().update({ + partial: true, + query: { + inspect: panel.id, + }, + }); + }; + const onDuplicatePanel = () => { duplicatePanel(dashboard, panel); }; @@ -78,6 +89,15 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { shortcut: 'p s', }); + if (config.featureToggles.inspect) { + menu.push({ + text: 'Inspect', + iconClassName: 'fa fa-fw fa-info-circle', + onClick: onInspectPanel, + shortcut: 'p i', + }); + } + const subMenu: PanelMenuItem[] = []; if (!panel.fullscreen && dashboard.meta.canEdit) { diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 4499805e08d..f4c3bdd6879 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -18,6 +18,7 @@ import { auto } from 'angular'; import { TemplateSrv } from '../templating/template_srv'; import { getPanelLinksSupplier } from './panellinks/linkSuppliers'; import { renderMarkdown, AppEvent, PanelEvents, PanelPluginMeta } from '@grafana/data'; +import { getLocationSrv } from '@grafana/runtime'; export class PanelCtrl { panel: any; @@ -145,6 +146,15 @@ export class PanelCtrl { shortcut: 'p s', }); + if (config.featureToggles.inspect) { + menu.push({ + text: 'Inspect', + icon: 'fa fa-fw fa-info-circle', + click: 'ctrl.inspectPanel();', + shortcut: 'p i', + }); + } + // Additional items from sub-class menu.push(...(await this.getAdditionalMenuItems())); @@ -234,6 +244,15 @@ export class PanelCtrl { sharePanelUtil(this.dashboard, this.panel); } + inspectPanel() { + getLocationSrv().update({ + query: { + inspect: this.panel.id, + }, + partial: true, + }); + } + getInfoMode() { if (this.error) { return 'error';