diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index ad2627488e7..5f8c9b437e8 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -136,6 +136,7 @@ export interface PanelMenuItem { iconClassName?: string; onClick?: (event: React.MouseEvent) => void; shortcut?: string; + href?: string; subMenu?: PanelMenuItem[]; } diff --git a/packages/grafana-data/src/types/panelEvents.ts b/packages/grafana-data/src/types/panelEvents.ts index 004eaa543c0..e64119c2862 100644 --- a/packages/grafana-data/src/types/panelEvents.ts +++ b/packages/grafana-data/src/types/panelEvents.ts @@ -1,5 +1,6 @@ import { eventFactory } from './utils'; import { DataQueryError, DataQueryResponseData } from './datasource'; +import { AngularPanelMenuItem } from './panel'; /** Payloads */ export interface PanelChangeViewPayload { @@ -9,13 +10,6 @@ export interface PanelChangeViewPayload { toggle?: boolean; } -export interface MenuElement { - text: string; - click: string; - role?: string; - shortcut?: string; -} - /** Events */ export const refresh = eventFactory('refresh'); @@ -24,7 +18,7 @@ export const dataError = eventFactory('data-error'); export const dataReceived = eventFactory('data-received'); export const dataSnapshotLoad = eventFactory('data-snapshot-load'); export const editModeInitialized = eventFactory('init-edit-mode'); -export const initPanelActions = eventFactory('init-panel-actions'); +export const initPanelActions = eventFactory('init-panel-actions'); export const panelChangeView = eventFactory('panel-change-view'); export const panelInitialized = eventFactory('panel-initialized'); export const panelSizeChanged = eventFactory('panel-size-changed'); diff --git a/public/app/features/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx index 2eef9b09e7a..f5a6354f4e3 100644 --- a/public/app/features/alerting/AlertTab.tsx +++ b/public/app/features/alerting/AlertTab.tsx @@ -14,7 +14,7 @@ import StateHistory from './StateHistory'; import 'app/features/alerting/AlertTabCtrl'; import { DashboardModel } from '../dashboard/state/DashboardModel'; -import { PanelModel } from '../dashboard/state/PanelModel'; +import { PanelModel, angularPanelUpdated } from '../dashboard/state/PanelModel'; import { TestRuleResult } from './TestRuleResult'; import { AppNotificationSeverity, StoreState } from 'app/types'; import { PanelEditorTabIds, getPanelEditorTab } from '../dashboard/panel_editor/state/reducers'; @@ -22,7 +22,6 @@ import { changePanelEditorTab } from '../dashboard/panel_editor/state/actions'; import { CoreEvents } from 'app/types'; interface Props { - angularPanel?: AngularComponent; dashboard: DashboardModel; panel: PanelModel; changePanelEditorTab: typeof changePanelEditorTab; @@ -42,19 +41,16 @@ class UnConnectedAlertTab extends PureComponent { }; componentDidMount() { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } + this.loadAlertTab(); + this.props.panel.events.on(angularPanelUpdated, this.onAngularPanelUpdated); } + onAngularPanelUpdated = () => { + this.forceUpdate(); + }; + componentDidUpdate(prevProps: Props) { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - shouldLoadAlertTab() { - return this.props.angularPanel && this.element && !this.component; + this.loadAlertTab(); } componentWillUnmount() { @@ -64,9 +60,13 @@ class UnConnectedAlertTab extends PureComponent { } async loadAlertTab() { - const { angularPanel, panel } = this.props; + const { panel } = this.props; - const scope = angularPanel.getScope(); + if (!this.element || !panel.angularPanel || this.component) { + return; + } + + const scope = panel.angularPanel.getScope(); // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet if (!scope.$$childHead) { diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index d70198e56af..8ec81cc2813 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -90,6 +90,7 @@ export class AnnotationsSrv { this.alertStatesPromise = getBackendSrv().get('/api/alerts/states-for-dashboard', { dashboardId: options.dashboard.id, }); + return this.alertStatesPromise; } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3d43f1b75a3..7b882921e25 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -183,6 +183,7 @@ export class DashboardPage extends PureComponent { try { this.props.dashboard.render(); } catch (err) { + console.error(err); this.props.notifyApp(createErrorNotification(`Panel rendering error`, err)); } } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index d82e46ce0d8..fe08799a931 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -4,7 +4,6 @@ import classNames from 'classnames'; import AutoSizer from 'react-virtualized-auto-sizer'; // Utils & Services -import { getAngularLoader, AngularComponent } from '@grafana/runtime'; import { importPanelPlugin } from 'app/features/plugins/plugin_loader'; // Components @@ -13,6 +12,7 @@ import { DashboardRow } from '../components/DashboardRow'; import { PanelChrome } from './PanelChrome'; import { PanelEditor } from '../panel_editor/PanelEditor'; import { PanelResizer } from './PanelResizer'; +import { PanelChromeAngular } from './PanelChromeAngular'; // Types import { PanelModel, DashboardModel } from '../state'; @@ -29,7 +29,6 @@ export interface Props { export interface State { plugin: PanelPlugin; - angularPanel: AngularComponent; isLazy: boolean; } @@ -42,7 +41,6 @@ export class DashboardPanel extends PureComponent { this.state = { plugin: null, - angularPanel: null, isLazy: !props.isInView, }; @@ -77,16 +75,13 @@ export class DashboardPanel extends PureComponent { if (!this.state.plugin || this.state.plugin.meta.id !== pluginId) { const plugin = await importPanelPlugin(pluginId); - // unmount angular panel - this.cleanUpAngularPanel(); - if (panel.type !== pluginId) { panel.changePlugin(plugin); } else { panel.pluginLoaded(plugin); } - this.setState({ plugin, angularPanel: null }); + this.setState({ plugin }); } } @@ -98,28 +93,6 @@ export class DashboardPanel extends PureComponent { if (this.state.isLazy && this.props.isInView) { this.setState({ isLazy: false }); } - - if (!this.element || this.state.angularPanel) { - return; - } - - const loader = getAngularLoader(); - const template = ''; - const scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard }; - const angularPanel = loader.load(this.element, scopeProps, template); - - this.setState({ angularPanel }); - } - - cleanUpAngularPanel() { - if (this.state.angularPanel) { - this.state.angularPanel.destroy(); - this.element = null; - } - } - - componentWillUnmount() { - this.cleanUpAngularPanel(); } onMouseEnter = () => { @@ -134,10 +107,6 @@ export class DashboardPanel extends PureComponent { const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props; const { plugin } = this.state; - if (plugin.angularPanelCtrl) { - return
(this.element = element)} className="panel-height-helper" />; - } - return ( {({ width, height }) => { @@ -145,6 +114,20 @@ export class DashboardPanel extends PureComponent { return null; } + if (plugin.angularPanelCtrl) { + return ( + + ); + } + return ( { render() { const { panel, dashboard, isFullscreen, isEditing } = this.props; - const { plugin, angularPanel, isLazy } = this.state; + const { plugin, isLazy } = this.state; if (this.isSpecial(panel.type)) { return this.specialPanels[panel.type](); @@ -212,7 +195,6 @@ export class DashboardPanel extends PureComponent { panel={panel} plugin={plugin} dashboard={dashboard} - angularPanel={angularPanel} onPluginTypeChange={this.onPluginTypeChange} /> )} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 2a833067cc6..4ca022858e3 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -272,7 +272,7 @@ export class PanelChrome extends PureComponent { // do not render component until we have first data if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { - return this.renderLoadingState(); + return null; } const PanelComponent = plugin.panel; @@ -290,7 +290,6 @@ export class PanelChrome extends PureComponent { return ( <> - {loading === LoadingState.Loading && this.renderLoadingState()}
{ ); } - private renderLoadingState(): JSX.Element { - return ( -
- -
- ); - } - hasOverlayHeader() { const { panel } = this.props; const { errorMessage, data } = this.state; @@ -360,6 +351,7 @@ export class PanelChrome extends PureComponent { links={panel.links} error={errorMessage} isFullscreen={isFullscreen} + isLoading={data.state === LoadingState.Loading} /> {({ error, errorInfo }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx new file mode 100644 index 00000000000..5ebabbb1698 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx @@ -0,0 +1,212 @@ +// Libraries +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import { Unsubscribable } from 'rxjs'; +// Components +import { PanelHeader } from './PanelHeader/PanelHeader'; +// Utils & Services +import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; +import { getAngularLoader } from '@grafana/runtime'; +// Types +import { DashboardModel, PanelModel } from '../state'; +import { LoadingState, DefaultTimeRange, PanelData, PanelPlugin, PanelEvents } from '@grafana/data'; + +export interface Props { + panel: PanelModel; + dashboard: DashboardModel; + plugin: PanelPlugin; + isFullscreen: boolean; + isInView: boolean; + width: number; + height: number; +} + +export interface State { + data: PanelData; + errorMessage?: string; + alertState?: string; +} + +interface AngularScopeProps { + panel: PanelModel; + dashboard: DashboardModel; + size: { + height: number; + width: number; + }; +} + +export class PanelChromeAngular extends PureComponent { + element?: HTMLElement; + timeSrv: TimeSrv = getTimeSrv(); + scopeProps?: AngularScopeProps; + querySubscription: Unsubscribable; + + constructor(props: Props) { + super(props); + this.state = { + data: { + state: LoadingState.NotStarted, + series: [], + timeRange: DefaultTimeRange, + }, + }; + } + + componentDidMount() { + const { panel } = this.props; + this.loadAngularPanel(); + + // subscribe to data events + const queryRunner = panel.getQueryRunner(); + this.querySubscription = queryRunner.getData(false).subscribe({ + next: (data: PanelData) => this.onPanelDataUpdate(data), + }); + } + + subscribeToRenderEvent() { + // Subscribe to render event, this is as far as I know only needed for changes to title & transparent + // These changes are modified in the model and only way to communicate that change is via this event + // Need to find another solution for this in tthe future (panel title in redux?) + this.props.panel.events.on(PanelEvents.render, this.onPanelRenderEvent); + } + + onPanelRenderEvent = (payload?: any) => { + const { alertState } = this.state; + + if (payload && payload.alertState) { + this.setState({ alertState: payload.alertState }); + } else if (payload && alertState) { + this.setState({ alertState: undefined }); + } else { + // only needed for detecting title updates right now fix before 7.0 + this.forceUpdate(); + } + }; + + onPanelDataUpdate(data: PanelData) { + let errorMessage: string | null = null; + + if (data.state === LoadingState.Error) { + const { error } = data; + if (error) { + if (errorMessage !== error.message) { + errorMessage = error.message; + } + } + } + + this.setState({ data, errorMessage }); + } + + componentWillUnmount() { + this.cleanUpAngularPanel(); + + if (this.querySubscription) { + this.querySubscription.unsubscribe(); + this.querySubscription = null; + } + + this.props.panel.events.off(PanelEvents.render, this.onPanelRenderEvent); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + if (prevProps.plugin !== this.props.plugin) { + this.cleanUpAngularPanel(); + this.loadAngularPanel(); + } + + this.loadAngularPanel(); + } + + loadAngularPanel() { + const { panel, dashboard, height, width } = this.props; + + // if we have no element or already have loaded the panel return + if (!this.element || panel.angularPanel) { + this.scopeProps.size.height = height; + this.scopeProps.size.width = width; + return; + } + + const loader = getAngularLoader(); + const template = ''; + + this.scopeProps = { + panel: panel, + dashboard: dashboard, + size: { width, height }, + }; + + // compile angular template and get back handle to scope + panel.setAngularPanel(loader.load(this.element, this.scopeProps, template)); + + // need to to this every time we load an angular as all events are unsubscribed when panel is destroyed + this.subscribeToRenderEvent(); + } + + cleanUpAngularPanel() { + const { panel } = this.props; + + if (panel.angularPanel) { + panel.setAngularPanel(undefined); + } + } + + hasOverlayHeader() { + const { panel } = this.props; + const { errorMessage, data } = this.state; + + // always show normal header if we have an error message + if (errorMessage) { + return false; + } + + // always show normal header if we have time override + if (data.request && data.request.timeInfo) { + return false; + } + + return !panel.hasTitle(); + } + + render() { + const { dashboard, panel, isFullscreen, plugin } = this.props; + const { errorMessage, data, alertState } = this.state; + const { transparent } = panel; + + const containerClassNames = classNames({ + 'panel-container': true, + 'panel-container--absolute': true, + 'panel-container--transparent': transparent, + 'panel-container--no-title': this.hasOverlayHeader(), + 'panel-has-alert': panel.alert !== undefined, + [`panel-alert-state--${alertState}`]: alertState !== undefined, + }); + + const panelContentClassNames = classNames({ + 'panel-content': true, + 'panel-content--no-padding': plugin.noPadding, + }); + + return ( +
+ +
+
(this.element = element)} className="panel-height-helper" /> +
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index e1f8a286821..e6dfbe3af17 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import classNames from 'classnames'; import { isEqual } from 'lodash'; -import { DataLink, ScopedVars } from '@grafana/data'; +import { DataLink, ScopedVars, PanelMenuItem } from '@grafana/data'; import { ClickOutsideWrapper } from '@grafana/ui'; import { e2e } from '@grafana/e2e'; @@ -12,6 +12,7 @@ import templateSrv from 'app/features/templating/template_srv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; +import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu'; export interface Props { panel: PanelModel; @@ -23,6 +24,7 @@ export interface Props { links?: DataLink[]; error?: string; isFullscreen: boolean; + isLoading: boolean; } interface ClickCoordinates { @@ -32,13 +34,15 @@ interface ClickCoordinates { interface State { panelMenuOpen: boolean; + menuItems: PanelMenuItem[]; } export class PanelHeader extends Component { clickCoordinates: ClickCoordinates = { x: 0, y: 0 }; - state = { + + state: State = { panelMenuOpen: false, - clickCoordinates: { x: 0, y: 0 }, + menuItems: [], }; eventToClickCoordinates = (event: React.MouseEvent) => { @@ -57,13 +61,19 @@ export class PanelHeader extends Component { }; onMenuToggle = (event: React.MouseEvent) => { - if (this.isClick(this.eventToClickCoordinates(event))) { - event.stopPropagation(); - - this.setState(prevState => ({ - panelMenuOpen: !prevState.panelMenuOpen, - })); + if (!this.isClick(this.eventToClickCoordinates(event))) { + return; } + + event.stopPropagation(); + + const { dashboard, panel } = this.props; + const menuItems = getPanelMenu(dashboard, panel); + + this.setState({ + panelMenuOpen: !this.state.panelMenuOpen, + menuItems, + }); }; closeMenu = () => { @@ -72,8 +82,17 @@ export class PanelHeader extends Component { }); }; + private renderLoadingState(): JSX.Element { + return ( +
+ +
+ ); + } + render() { - const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props; + const { panel, timeInfo, scopedVars, error, isFullscreen, isLoading } = this.props; + const { menuItems } = this.state; const title = templateSrv.replaceWithText(panel.title, scopedVars); const panelHeaderClass = classNames({ @@ -83,6 +102,7 @@ export class PanelHeader extends Component { return ( <> + {isLoading && this.renderLoadingState()}
{ {this.state.panelMenuOpen && ( - + )} {timeInfo && ( diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 7c2fc90882b..c180359922a 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,13 +1,9 @@ import React, { PureComponent } from 'react'; -import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; -import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu'; import { PanelMenuItem } from '@grafana/data'; export interface Props { - panel: PanelModel; - dashboard: DashboardModel; + items: PanelMenuItem[]; } export class PanelHeaderMenu extends PureComponent { @@ -33,9 +29,6 @@ export class PanelHeaderMenu extends PureComponent { }; render() { - const { dashboard, panel } = this.props; - const menu = getPanelMenu(dashboard, panel); - - return
{this.renderItems(menu)}
; + return
{this.renderItems(this.props.items)}
; } } diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx index 531b0617d97..99e54465a4d 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -13,7 +13,7 @@ export const PanelHeaderMenuItem: FC = props => {
  • ) : (
  • - + {props.iconClassName && } void; +} + +export class AngularPanelOptions extends PureComponent { + element?: HTMLElement; + angularOptions: AngularComponent; + + constructor(props: Props) { + super(props); + } + + componentDidMount() { + this.loadAngularOptions(); + this.props.panel.events.on(angularPanelUpdated, this.onAngularPanelUpdated); + } + + onAngularPanelUpdated = () => { + this.forceUpdate(); + }; + + componentDidUpdate(prevProps: Props) { + if (this.props.plugin !== prevProps.plugin) { + this.cleanUpAngularOptions(); + } + + this.loadAngularOptions(); + } + + componentWillUnmount() { + this.cleanUpAngularOptions(); + this.props.panel.events.off(angularPanelUpdated, this.onAngularPanelUpdated); + } + + cleanUpAngularOptions() { + if (this.angularOptions) { + this.angularOptions.destroy(); + this.angularOptions = null; + } + } + + loadAngularOptions() { + const { panel } = this.props; + + if (!this.element || !panel.angularPanel || this.angularOptions) { + return; + } + + const scope = panel.angularPanel.getScope(); + + // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet + if (!scope.$$childHead) { + setTimeout(() => { + this.forceUpdate(); + }); + return; + } + + const panelCtrl: PanelCtrl = scope.$$childHead.ctrl; + panelCtrl.initEditMode(); + panelCtrl.onPluginTypeChange = this.props.onPluginTypeChange; + + let template = ''; + for (let i = 0; i < panelCtrl.editorTabs.length; i++) { + template += + ` +
    ` + + (i > 0 + ? `
    + {{ctrl.editorTabs[${i}].title}} + +
    ` + : '') + + `
    + +
    +
    + `; + } + + const loader = getAngularLoader(); + const scopeProps = { ctrl: panelCtrl }; + + this.angularOptions = loader.load(this.element, scopeProps, template); + } + + render() { + return
    (this.element = elem)} />; + } +} diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 1638e0e2d95..41e1125fcec 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -4,7 +4,7 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { Tooltip } from '@grafana/ui'; import { PanelPlugin, PanelPluginMeta } from '@grafana/data'; -import { AngularComponent, config } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { e2e } from '@grafana/e2e'; import { QueriesTab } from './QueriesTab'; @@ -22,7 +22,6 @@ interface PanelEditorProps { panel: PanelModel; dashboard: DashboardModel; plugin: PanelPlugin; - angularPanel?: AngularComponent; onPluginTypeChange: (newType: PanelPluginMeta) => void; activeTab: PanelEditorTabIds; tabs: PanelEditorTab[]; @@ -71,15 +70,14 @@ class UnConnectedPanelEditor extends PureComponent { }; renderCurrentTab(activeTab: string) { - const { panel, dashboard, plugin, angularPanel } = this.props; - + const { panel, dashboard, plugin } = this.props; switch (activeTab) { case 'advanced': return ; case 'queries': return ; case 'alert': - return ; + return ; case 'visualization': return ( { dashboard={dashboard} plugin={plugin} onPluginTypeChange={this.onPluginTypeChange} - angularPanel={angularPanel} /> ); default: diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 8a72d724236..6e5cacfde26 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -1,8 +1,8 @@ // Libraries import React, { PureComponent } from 'react'; // Utils & Services -import { AngularComponent, getAngularLoader } from '@grafana/runtime'; -import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; +import { AngularComponent } from '@grafana/runtime'; +import { connect } from 'react-redux'; import { StoreState } from 'app/types'; import { updateLocation } from 'app/core/actions'; // Components @@ -10,11 +10,11 @@ import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; +import { AngularPanelOptions } from './AngularPanelOptions'; // Types import { PanelModel, DashboardModel } from '../state'; import { VizPickerSearch } from './VizPickerSearch'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; -import { PanelCtrl } from 'app/plugins/sdk'; import { Unsubscribable } from 'rxjs'; import { PanelPlugin, PanelPluginMeta, PanelData, LoadingState, DefaultTimeRange } from '@grafana/data'; @@ -22,7 +22,6 @@ interface Props { panel: PanelModel; dashboard: DashboardModel; plugin: PanelPlugin; - angularPanel?: AngularComponent; onPluginTypeChange: (newType: PanelPluginMeta) => void; updateLocation: typeof updateLocation; urlOpenVizPicker: boolean; @@ -63,10 +62,17 @@ export class VisualizationTab extends PureComponent { }; renderPanelOptions() { - const { plugin, angularPanel } = this.props; + const { plugin, dashboard, panel } = this.props; - if (angularPanel) { - return
    (this.element = element)} />; + if (plugin.angularPanelCtrl) { + return ( + + ); } if (plugin.editor) { @@ -85,82 +91,16 @@ export class VisualizationTab extends PureComponent { componentDidMount() { const { panel } = this.props; const queryRunner = panel.getQueryRunner(); - if (this.shouldLoadAngularOptions()) { - this.loadAngularOptions(); - } this.querySubscription = queryRunner.getData().subscribe({ next: (data: PanelData) => this.setState({ data }), }); } - componentDidUpdate(prevProps: Props) { - if (this.props.plugin !== prevProps.plugin) { - this.cleanUpAngularOptions(); - } - - if (this.shouldLoadAngularOptions()) { - this.loadAngularOptions(); - } - } - - shouldLoadAngularOptions() { - return this.props.angularPanel && this.element && !this.angularOptions; - } - - loadAngularOptions() { - const { angularPanel } = this.props; - - const scope = angularPanel.getScope(); - - // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet - if (!scope.$$childHead) { - setTimeout(() => { - this.forceUpdate(); - }); - return; - } - - const panelCtrl: PanelCtrl = scope.$$childHead.ctrl; - panelCtrl.initEditMode(); - panelCtrl.onPluginTypeChange = this.onPluginTypeChange; - - let template = ''; - for (let i = 0; i < panelCtrl.editorTabs.length; i++) { - template += - ` -
    ` + - (i > 0 - ? `
    - {{ctrl.editorTabs[${i}].title}} - -
    ` - : '') + - `
    - -
    -
    - `; - } - - const loader = getAngularLoader(); - const scopeProps = { ctrl: panelCtrl }; - - this.angularOptions = loader.load(this.element, scopeProps, template); - } - componentWillUnmount() { if (this.querySubscription) { this.querySubscription.unsubscribe(); } - this.cleanUpAngularOptions(); - } - - cleanUpAngularOptions() { - if (this.angularOptions) { - this.angularOptions.destroy(); - this.angularOptions = null; - } } clearQuery = () => { @@ -276,4 +216,4 @@ const mapDispatchToProps = { updateLocation, }; -export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps); +export default connect(mapStateToProps, mapDispatchToProps)(VisualizationTab); diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index e39f9267695..59bf918bde5 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -1,6 +1,5 @@ import { PanelModel } from './PanelModel'; import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks'; -import { PanelEvents } from '@grafana/data'; class TablePanelCtrl {} @@ -148,18 +147,19 @@ describe('PanelModel', () => { }); describe('when changing from angular panel', () => { - let tearDownPublished = false; + const angularPanel = { + scope: {}, + destroy: jest.fn(), + }; beforeEach(() => { - model.events.on(PanelEvents.panelTeardown, () => { - tearDownPublished = true; - }); + model.angularPanel = angularPanel; model.changePlugin(getPanelPlugin({ id: 'graph' })); }); - it('should teardown / destroy panel so angular panels event subscriptions are removed', () => { - expect(tearDownPublished).toBe(true); - expect(model.events.getEventCount()).toBe(0); + it('should set angularPanel to undefined and call destory', () => { + expect(angularPanel.destroy.mock.calls.length).toBe(1); + expect(model.angularPanel).toBe(undefined); }); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 95f23fb05d8..fbce6669995 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -13,6 +13,7 @@ import { DataTransformerConfig, ScopedVars, } from '@grafana/data'; +import { AngularComponent } from '@grafana/runtime'; import config from 'app/core/config'; @@ -22,6 +23,7 @@ import { take } from 'rxjs/operators'; export const panelAdded = eventFactory('panel-added'); export const panelRemoved = eventFactory('panel-removed'); +export const angularPanelUpdated = eventFactory('panel-angular-panel-updated'); export interface GridPos { x: number; @@ -41,6 +43,7 @@ const notPersistedProperties: { [str: string]: boolean } = { cachedPluginOptions: true, plugin: true, queryRunner: true, + angularPanel: true, restoreModel: true, }; @@ -135,6 +138,8 @@ export class PanelModel { cachedPluginOptions?: any; legend?: { show: boolean }; plugin?: PanelPlugin; + angularPanel?: AngularComponent; + private queryRunner?: PanelQueryRunner; constructor(model: any) { @@ -290,9 +295,8 @@ export class PanelModel { const oldPluginId = this.type; const wasAngular = !!this.plugin.angularPanelCtrl; - // for angular panels we must remove all events and let angular panels do some cleanup - if (wasAngular) { - this.destroy(); + if (this.angularPanel) { + this.setAngularPanel(undefined); } // remove panel type specific options @@ -380,19 +384,32 @@ export class PanelModel { } destroy() { - this.events.emit(PanelEvents.panelTeardown); this.events.removeAllListeners(); if (this.queryRunner) { this.queryRunner.destroy(); this.queryRunner = null; } + + if (this.angularPanel) { + this.angularPanel.destroy(); + } } setTransformations(transformations: DataTransformerConfig[]) { this.transformations = transformations; this.getQueryRunner().setTransformations(transformations); } + + setAngularPanel(component: AngularComponent) { + if (this.angularPanel) { + // this will remove all event listeners + this.angularPanel.destroy(); + } + + this.angularPanel = component; + this.events.emit(angularPanelUpdated); + } } function getPluginVersion(plugin: PanelPlugin): string { diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 7a4ef3a71c1..feff9496b9a 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -26,13 +26,13 @@ const dashbardSlice = createSlice({ loadDashboardPermissions: (state, action: PayloadAction) => { state.permissions = processAclItems(action.payload); }, - dashboardInitFetching: state => { + dashboardInitFetching: (state, action: PayloadAction) => { state.initPhase = DashboardInitPhase.Fetching; }, - dashboardInitServices: state => { + dashboardInitServices: (state, action: PayloadAction) => { state.initPhase = DashboardInitPhase.Services; }, - dashboardInitSlow: state => { + dashboardInitSlow: (state, action: PayloadAction) => { state.isInitSlow = true; }, dashboardInitCompleted: (state, action: PayloadAction) => { @@ -41,16 +41,13 @@ const dashbardSlice = createSlice({ state.isInitSlow = false; }, dashboardInitFailed: (state, action: PayloadAction) => { - const failedDashboard = new DashboardModel( - { title: 'Dashboard init failed' }, - { canSave: false, canEdit: false } - ); - state.initPhase = DashboardInitPhase.Failed; state.initError = action.payload; - state.getModel = () => failedDashboard; + state.getModel = () => { + return new DashboardModel({ title: 'Dashboard init failed' }, { canSave: false, canEdit: false }); + }; }, - cleanUpDashboard: state => { + cleanUpDashboard: (state, action: PayloadAction) => { if (state.getModel()) { state.getModel().destroy(); state.getModel = () => null; @@ -63,7 +60,7 @@ const dashbardSlice = createSlice({ setDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction) => { state.modifiedQueries = action.payload; }, - clearDashboardQueriesToUpdateOnLoad: state => { + clearDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction) => { state.modifiedQueries = null; }, }, diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts new file mode 100644 index 00000000000..d2658d4b074 --- /dev/null +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -0,0 +1,63 @@ +import { PanelModel, DashboardModel } from '../state'; +import { getPanelMenu } from './getPanelMenu'; + +describe('getPanelMenu', () => { + it('should return the correct panel menu items', () => { + const panel = new PanelModel({}); + const dashboard = new DashboardModel({}); + + const menuItems = getPanelMenu(dashboard, panel); + expect(menuItems).toMatchInlineSnapshot(` + Array [ + Object { + "iconClassName": "gicon gicon-viewer", + "onClick": [Function], + "shortcut": "v", + "text": "View", + }, + Object { + "iconClassName": "gicon gicon-editor", + "onClick": [Function], + "shortcut": "e", + "text": "Edit", + }, + Object { + "iconClassName": "fa fa-fw fa-share", + "onClick": [Function], + "shortcut": "p s", + "text": "Share", + }, + Object { + "iconClassName": "fa fa-fw fa-cube", + "onClick": [Function], + "subMenu": Array [ + Object { + "onClick": [Function], + "shortcut": "p d", + "text": "Duplicate", + }, + Object { + "onClick": [Function], + "text": "Copy", + }, + Object { + "onClick": [Function], + "text": "Panel JSON", + }, + ], + "text": "More...", + "type": "submenu", + }, + Object { + "type": "divider", + }, + Object { + "iconClassName": "fa fa-fw fa-trash", + "onClick": [Function], + "shortcut": "p r", + "text": "Remove", + }, + ] + `); + }); +}); diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 521046740bd..c84e58df0d1 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -3,7 +3,6 @@ import { store } from 'app/store/store'; import config from 'app/core/config'; import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime'; import { PanelMenuItem } from '@grafana/data'; - import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; @@ -11,8 +10,9 @@ import { contextSrv } from '../../../core/services/context_srv'; import { navigateToExplore } from '../../explore/state/actions'; import { getExploreUrl } from '../../../core/utils/explore'; import { getTimeSrv } from '../services/TimeSrv'; +import { PanelCtrl } from '../../panel/panel_ctrl'; -export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { +export function getPanelMenu(dashboard: DashboardModel, panel: PanelModel): PanelMenuItem[] { const onViewPanel = (event: React.MouseEvent) => { event.preventDefault(); store.dispatch( @@ -123,7 +123,7 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { shortcut: 'p s', }); - if (contextSrv.hasAccessToExplore() && panel.datasource) { + if (contextSrv.hasAccessToExplore() && !panel.plugin.meta.skipDataQuery) { menu.push({ text: 'Explore', iconClassName: 'gicon gicon-explore', @@ -170,6 +170,29 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { onClick: onEditPanelJson, }); + // add old angular panel options + if (panel.angularPanel) { + const scope = panel.angularPanel.getScope(); + const panelCtrl: PanelCtrl = scope.$$childHead.ctrl; + const angularMenuItems = panelCtrl.getExtendedMenu(); + + for (const item of angularMenuItems) { + const reactItem: PanelMenuItem = { + text: item.text, + href: item.href, + shortcut: item.shortcut, + }; + + if (item.click) { + reactItem.onClick = () => { + scope.$eval(item.click, { ctrl: panelCtrl }); + }; + } + + subMenu.push(reactItem); + } + } + menu.push({ type: 'submenu', text: 'More...', @@ -190,4 +213,4 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { } return menu; -}; +} diff --git a/public/app/features/panel/all.ts b/public/app/features/panel/all.ts index 9fb855197f3..be6bf9027be 100644 --- a/public/app/features/panel/all.ts +++ b/public/app/features/panel/all.ts @@ -1,4 +1,3 @@ -import './panel_header'; import './panel_directive'; import './query_ctrl'; import './panel_editor_tab'; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 84dbb41c89a..ebb82cea870 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -1,10 +1,6 @@ import _ from 'lodash'; - -import kbn from 'app/core/utils/kbn'; - import { PanelCtrl } from 'app/features/panel/panel_ctrl'; -import { getExploreUrl } from 'app/core/utils/explore'; -import { applyPanelTimeOverrides, getResolution } from 'app/features/dashboard/utils/panel'; +import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { ContextSrv } from 'app/core/services/context_srv'; import { DataFrame, @@ -164,32 +160,14 @@ class MetricsPanelCtrl extends PanelCtrl { updateTimeRange(datasource?: DataSourceApi) { this.datasource = datasource || this.datasource; this.range = this.timeSrv.timeRange(); - this.resolution = getResolution(this.panel); const newTimeData = applyPanelTimeOverrides(this.panel, this.range); this.timeInfo = newTimeData.timeInfo; this.range = newTimeData.timeRange; - this.calculateInterval(); - return this.datasource; } - calculateInterval() { - let intervalOverride = this.panel.interval; - - // if no panel interval check datasource - if (intervalOverride) { - intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars); - } else if (this.datasource && this.datasource.interval) { - intervalOverride = this.datasource.interval; - } - - const res = kbn.calculateInterval(this.range, this.resolution, intervalOverride); - this.interval = res.interval; - this.intervalMs = res.intervalMs; - } - issueQueries(datasource: DataSourceApi) { this.datasource = datasource; @@ -206,8 +184,9 @@ class MetricsPanelCtrl extends PanelCtrl { panelId: panel.id, dashboardId: this.dashboard.id, timezone: this.dashboard.timezone, + timeInfo: this.timeInfo, timeRange: this.range, - widthPixels: this.resolution, // The pixel width + widthPixels: this.width, maxDataPoints: panel.maxDataPoints, minInterval: panel.interval, scopedVars: panel.scopedVars, @@ -248,25 +227,6 @@ class MetricsPanelCtrl extends PanelCtrl { this.processDataError(err); } } - - async getAdditionalMenuItems() { - const items = []; - if (this.contextSrv.hasAccessToExplore() && this.datasource) { - items.push({ - text: 'Explore', - icon: 'gicon gicon-explore', - shortcut: 'x', - href: await getExploreUrl({ - panel: this.panel, - panelTargets: this.panel.targets, - panelDatasource: this.datasource, - datasourceSrv: this.datasourceSrv, - timeSrv: this.timeSrv, - }), - }); - } - return items; - } } export { MetricsPanelCtrl }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 60559e1c2c3..611176c1eef 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -1,23 +1,9 @@ import _ from 'lodash'; -import { escapeHtml, sanitize } from 'app/core/utils/text'; - import config from 'app/core/config'; -import { Emitter, profiler } from 'app/core/core'; -import getFactors from 'app/core/utils/factors'; -import { - calculateInnerPanelHeight, - copyPanel as copyPanelUtil, - duplicatePanel, - editPanelJson as editPanelJsonUtil, - removePanel, - sharePanel as sharePanelUtil, -} from 'app/features/dashboard/utils/panel'; -import { GRID_COLUMN_COUNT } from 'app/core/constants'; +import { profiler } from 'app/core/core'; +import { Emitter } from 'app/core/utils/emitter'; import { auto } from 'angular'; -import { TemplateSrv } from '../templating/template_srv'; -import { getPanelLinksSupplier } from './panellinks/linkSuppliers'; -import { AppEvent, PanelEvents, PanelPluginMeta, renderMarkdown } from '@grafana/data'; -import { getLocationSrv } from '@grafana/runtime'; +import { AppEvent, PanelEvents, PanelPluginMeta, AngularPanelMenuItem } from '@grafana/data'; import { DashboardModel } from '../dashboard/state'; export class PanelCtrl { @@ -32,12 +18,12 @@ export class PanelCtrl { $location: any; $timeout: any; editModeInitiated: boolean; - height: any; + height: number; + width: number; containerHeight: any; events: Emitter; loading: boolean; timing: any; - maxPanelsPerRowOptions: number[]; constructor($scope: any, $injector: auto.IInjectorService) { this.$injector = $injector; @@ -74,31 +60,10 @@ export class PanelCtrl { this.$scope.$root.appEvent(event, payload); } - changeView(fullscreen: boolean, edit: boolean) { - this.publishAppEvent(PanelEvents.panelChangeView, { - fullscreen, - edit, - panelId: this.panel.id, - }); - } - - viewPanel() { - this.changeView(true, false); - } - - editPanel() { - this.changeView(true, true); - } - - exitFullscreen() { - this.changeView(false, false); - } - initEditMode() { if (!this.editModeInitiated) { this.editModeInitiated = true; this.events.emit(PanelEvents.editModeInitialized); - this.maxPanelsPerRowOptions = getFactors(GRID_COLUMN_COUNT); } } @@ -118,88 +83,8 @@ export class PanelCtrl { } } - async getMenu() { - const menu = []; - menu.push({ - text: 'View', - click: 'ctrl.viewPanel();', - icon: 'gicon gicon-viewer', - shortcut: 'v', - }); - - if (this.dashboard.canEditPanel(this.panel)) { - menu.push({ - text: 'Edit', - click: 'ctrl.editPanel();', - role: 'Editor', - icon: 'gicon gicon-editor', - shortcut: 'e', - }); - } - - menu.push({ - text: 'Share', - click: 'ctrl.sharePanel();', - icon: 'fa fa-fw fa-share', - 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())); - - const extendedMenu = this.getExtendedMenu(); - menu.push({ - text: 'More ...', - click: '', - icon: 'fa fa-fw fa-cube', - submenu: extendedMenu, - }); - - if (this.dashboard.canEditPanel(this.panel)) { - menu.push({ divider: true, role: 'Editor' }); - menu.push({ - text: 'Remove', - click: 'ctrl.removePanel();', - role: 'Editor', - icon: 'fa fa-fw fa-trash', - shortcut: 'p r', - }); - } - - return menu; - } - getExtendedMenu() { - const menu = []; - if (!this.panel.fullscreen && this.dashboard.canEditPanel(this.panel)) { - menu.push({ - text: 'Duplicate', - click: 'ctrl.duplicate()', - role: 'Editor', - shortcut: 'p d', - }); - - menu.push({ - text: 'Copy', - click: 'ctrl.copyPanel()', - role: 'Editor', - }); - } - - menu.push({ - text: 'Panel JSON', - click: 'ctrl.editPanelJson(); dismiss();', - }); - + const menu: AngularPanelMenuItem[] = []; this.events.emit(PanelEvents.initPanelActions, menu); return menu; } @@ -213,93 +98,10 @@ export class PanelCtrl { return this.dashboard.meta.fullscreen && !this.panel.fullscreen; } - calculatePanelHeight(containerHeight: number) { - this.containerHeight = containerHeight; - this.height = calculateInnerPanelHeight(this.panel, containerHeight); - } - render(payload?: any) { this.events.emit(PanelEvents.render, payload); } - duplicate() { - duplicatePanel(this.dashboard, this.panel); - } - - removePanel() { - removePanel(this.dashboard, this.panel, true); - } - - editPanelJson() { - editPanelJsonUtil(this.dashboard, this.panel); - } - - copyPanel() { - copyPanelUtil(this.panel); - } - - sharePanel() { - sharePanelUtil(this.dashboard, this.panel); - } - - inspectPanel() { - getLocationSrv().update({ - query: { - inspect: this.panel.id, - }, - partial: true, - }); - } - - getInfoMode() { - if (this.error) { - return 'error'; - } - if (!!this.panel.description) { - return 'info'; - } - if (this.panel.links && this.panel.links.length) { - return 'links'; - } - return ''; - } - - getInfoContent(options: { mode: string }) { - const { panel } = this; - let markdown = panel.description || ''; - - if (options.mode === 'tooltip') { - markdown = this.error || panel.description || ''; - } - - const templateSrv: TemplateSrv = this.$injector.get('templateSrv'); - const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars); - let html = '
    '; - - return config.disableSanitizeHtml ? html : sanitize(html); - } - // overriden from react onPluginTypeChange = (plugin: PanelPluginMeta) => {}; } diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 517a2d89af7..85511280fa4 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -1,35 +1,14 @@ import angular from 'angular'; -import $ from 'jquery'; -// @ts-ignore -import Drop from 'tether-drop'; // @ts-ignore import baron from 'baron'; import { PanelEvents } from '@grafana/data'; -import { getLocationSrv } from '@grafana/runtime'; -import { e2e } from '@grafana/e2e'; +import { PanelModel } from '../dashboard/state'; +import { PanelCtrl } from './panel_ctrl'; const module = angular.module('grafana.directives'); const panelTemplate = ` -
    -
    - - - - - - - - - - -
    - -
    - -
    -
    -
    + `; module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { @@ -39,52 +18,19 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { transclude: true, scope: { ctrl: '=' }, link: (scope: any, elem) => { - const panelContainer = elem.find('.panel-container'); - const panelContent = elem.find('.panel-content'); - const cornerInfoElem = elem.find('.panel-info-corner'); - const ctrl = scope.ctrl; - ctrl.selectors = e2e.pages.Dashboard.Panels.Panel.selectors; - let infoDrop: any; + const ctrl: PanelCtrl = scope.ctrl; + const panel: PanelModel = scope.ctrl.panel; + let panelScrollbar: any; - // the reason for handling these classes this way is for performance - // limit the watchers on panels etc - let transparentLastState = false; - let lastHasAlertRule = false; - let lastAlertState: boolean; - let hasAlertRule; - - function mouseEnter() { - panelContainer.toggleClass('panel-hover-highlight', true); - ctrl.dashboard.setPanelFocus(ctrl.panel.id); - } - - function mouseLeave() { - panelContainer.toggleClass('panel-hover-highlight', false); - ctrl.dashboard.setPanelFocus(0); - } - function resizeScrollableContent() { if (panelScrollbar) { panelScrollbar.update(); } } - function infoCornerClicked() { - if (ctrl.error) { - getLocationSrv().update({ partial: true, query: { inspect: ctrl.panel.id } }); - } - } - - // set initial transparency - if (ctrl.panel.transparent) { - transparentLastState = true; - panelContainer.addClass('panel-container--transparent'); - } - - // update scrollbar after mounting ctrl.events.on(PanelEvents.componentDidMount, () => { - if (ctrl.__proto__.constructor.scrollable) { + if ((ctrl as any).__proto__.constructor.scrollable) { const scrollRootClass = 'baron baron__root baron__clipper panel-content--scrollable'; const scrollerClass = 'baron__scroller'; const scrollBarHTML = ` @@ -93,8 +39,8 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
    `; - const scrollRoot = panelContent; - const scroller = panelContent.find(':first').find(':first'); + const scrollRoot = elem; + const scroller = elem.find(':first').find(':first'); scrollRoot.addClass(scrollRootClass); $(scrollBarHTML).appendTo(scrollRoot); @@ -112,112 +58,44 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { } }); - ctrl.events.on(PanelEvents.panelSizeChanged, () => { - ctrl.calculatePanelHeight(panelContainer[0].offsetHeight); + function onPanelSizeChanged() { $timeout(() => { resizeScrollableContent(); ctrl.render(); }); - }); + } - ctrl.events.on(PanelEvents.viewModeChanged, () => { + function onViewModeChanged() { // first wait one pass for dashboard fullscreen view mode to take effect (classses being applied) setTimeout(() => { - // then recalc style - ctrl.calculatePanelHeight(panelContainer[0].offsetHeight); // then wait another cycle (this might not be needed) $timeout(() => { ctrl.render(); resizeScrollableContent(); }); }, 10); - }); - - ctrl.events.on(PanelEvents.render, () => { - // set initial height - if (!ctrl.height) { - ctrl.calculatePanelHeight(panelContainer[0].offsetHeight); - } - - if (transparentLastState !== ctrl.panel.transparent) { - panelContainer.toggleClass('panel-container--transparent', ctrl.panel.transparent === true); - transparentLastState = ctrl.panel.transparent; - } - - hasAlertRule = ctrl.panel.alert !== undefined; - if (lastHasAlertRule !== hasAlertRule) { - panelContainer.toggleClass('panel-has-alert', hasAlertRule); - - lastHasAlertRule = hasAlertRule; - } - - if (ctrl.alertState) { - if (lastAlertState) { - panelContainer.removeClass('panel-alert-state--' + lastAlertState); - } - - if ( - ctrl.alertState.state === 'ok' || - ctrl.alertState.state === 'alerting' || - ctrl.alertState.state === 'pending' - ) { - panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state); - } - - lastAlertState = ctrl.alertState.state; - } else if (lastAlertState) { - panelContainer.removeClass('panel-alert-state--' + lastAlertState); - lastAlertState = null; - } - }); - - function updatePanelCornerInfo() { - const cornerMode = ctrl.getInfoMode(); - cornerInfoElem[0].className = 'panel-info-corner panel-info-corner--' + cornerMode; - - if (cornerMode) { - if (infoDrop) { - infoDrop.destroy(); - } - - infoDrop = new Drop({ - target: cornerInfoElem[0], - content: () => { - return ctrl.getInfoContent({ mode: 'tooltip' }); - }, - classes: ctrl.error ? 'drop-error' : 'drop-help', - openOn: 'hover', - hoverOpenDelay: 100, - tetherOptions: { - attachment: 'bottom left', - targetAttachment: 'top left', - constraints: [ - { - to: 'window', - attachment: 'together', - pin: true, - }, - ], - }, - }); - } } - scope.$watchGroup(['ctrl.error', 'ctrl.panel.description'], updatePanelCornerInfo); - scope.$watchCollection('ctrl.panel.links', updatePanelCornerInfo); + function onPanelModelRender(payload?: any) { + ctrl.height = scope.$parent.$parent.size.height; + ctrl.width = scope.$parent.$parent.size.width; + } - elem.on('mouseenter', mouseEnter); - elem.on('mouseleave', mouseLeave); + function onPanelModelRefresh() { + ctrl.height = scope.$parent.$parent.size.height; + ctrl.width = scope.$parent.$parent.size.width; + } - cornerInfoElem.on('click', infoCornerClicked); + panel.events.on(PanelEvents.refresh, onPanelModelRefresh); + panel.events.on(PanelEvents.render, onPanelModelRender); + panel.events.on(PanelEvents.panelSizeChanged, onPanelSizeChanged); + panel.events.on(PanelEvents.viewModeChanged, onViewModeChanged); scope.$on('$destroy', () => { elem.off(); - cornerInfoElem.off(); - if (infoDrop) { - infoDrop.destroy(); - } + panel.events.emit(PanelEvents.panelTeardown); + panel.events.removeAllListeners(); if (panelScrollbar) { panelScrollbar.dispose(); @@ -226,17 +104,3 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { }, }; }); - -module.directive('panelHelpCorner', $rootScope => { - return { - restrict: 'E', - template: ` - - - - - - `, - link: (scope, elem) => {}, - }; -}); diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts deleted file mode 100644 index 7284a772392..00000000000 --- a/public/app/features/panel/panel_header.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { coreModule } from 'app/core/core'; -import { AngularPanelMenuItem } from '@grafana/data'; -import { e2e } from '@grafana/e2e'; - -const template = ` - - - {{ctrl.panel.title | interpolateTemplateVars:this}} - - - - - {{ctrl.timeInfo}} -`; - -function renderMenuItem(item: AngularPanelMenuItem, ctrl: any) { - let html = ''; - let listItemClass = ''; - - if (item.divider) { - return '
  • '; - } - - if (item.submenu) { - listItemClass = 'dropdown-submenu'; - } - - html += `
  • `; - html += `${item.text}`; - - if (item.shortcut) { - html += `${item.shortcut}`; - } - - html += ``; - - if (item.submenu) { - html += ''; - } - - html += `
  • `; - return html; -} - -async function createMenuTemplate(ctrl: any) { - let html = ''; - - for (const item of await ctrl.getMenu()) { - html += renderMenuItem(item, ctrl); - } - - return html; -} - -/** @ngInject */ -function panelHeader($compile: any) { - return { - restrict: 'E', - template: template, - link: (scope: any, elem: any, attrs: any) => { - const menuElem = elem.find('.panel-menu'); - let menuScope: any; - let isDragged: boolean; - - elem.click(async (evt: any) => { - const targetClass = evt.target.className; - - // remove existing scope - if (menuScope) { - menuScope.$destroy(); - } - - menuScope = scope.$new(); - const menuHtml = await createMenuTemplate(scope.ctrl); - menuElem.html(menuHtml); - $compile(menuElem)(menuScope); - - if (targetClass.indexOf('panel-title-text') >= 0 || targetClass.indexOf('panel-title') >= 0) { - togglePanelMenu(evt); - } - }); - - function togglePanelMenu(e: any) { - if (!isDragged) { - e.stopPropagation(); - elem.find('[data-toggle=dropdown]').dropdown('toggle'); - } - } - - let mouseX: number, mouseY: number; - elem.mousedown((e: any) => { - mouseX = e.pageX; - mouseY = e.pageY; - }); - - elem.mouseup((e: any) => { - if (mouseX === e.pageX && mouseY === e.pageY) { - isDragged = false; - } else { - isDragged = true; - } - }); - }, - }; -} - -coreModule.directive('panelHeader', panelHeader); diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts index 3acbb694b74..be7a548562a 100644 --- a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts +++ b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts @@ -22,31 +22,10 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { MetricsPanelCtrl } from '../metrics_panel_ctrl'; describe('MetricsPanelCtrl', () => { - describe('when getting additional menu items', () => { - describe('and has no datasource set but user has access to explore', () => { - it('should not return any items', async () => { - const ctrl = setupController({ hasAccessToExplore: true }); - - expect((await ctrl.getAdditionalMenuItems()).length).toBe(0); - }); - }); - - describe('and has datasource set that supports explore and user does not have access to explore', () => { - it('should not return any items', async () => { - const ctrl = setupController({ hasAccessToExplore: false }); - ctrl.datasource = { meta: { explore: true } } as any; - - expect((await ctrl.getAdditionalMenuItems()).length).toBe(0); - }); - }); - - describe('and has datasource set that supports explore and user has access to explore', () => { - it('should return one item', async () => { - const ctrl = setupController({ hasAccessToExplore: true }); - ctrl.datasource = { meta: { explore: true } } as any; - - expect((await ctrl.getAdditionalMenuItems()).length).toBe(1); - }); + describe('can setup', () => { + it('should return controller', async () => { + const ctrl = setupController({ hasAccessToExplore: true }); + expect((await ctrl.getAdditionalMenuItems()).length).toBe(0); }); }); }); diff --git a/public/app/plugins/datasource/influxdb/influx_query_model.ts b/public/app/plugins/datasource/influxdb/influx_query_model.ts index cdd50a397b8..5cd085860a1 100644 --- a/public/app/plugins/datasource/influxdb/influx_query_model.ts +++ b/public/app/plugins/datasource/influxdb/influx_query_model.ts @@ -62,7 +62,12 @@ export default class InfluxQueryModel { } addGroupBy(value: string) { - const stringParts = value.match(/^(\w+)\((.*)\)$/); + let stringParts = value.match(/^(\w+)\((.*)\)$/); + + if (!stringParts || !this.target.groupBy) { + return; + } + const typePart = stringParts[1]; const arg = stringParts[2]; const partModel = queryPart.create({ type: typePart, params: [arg] }); diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index 9a6439c5468..8e619d14807 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -128,36 +128,33 @@ function checkOppositeSides(yLeft: AxisSide, yRight: AxisSide) { return (yLeft.min >= 0 && yRight.max <= 0) || (yLeft.max <= 0 && yRight.min >= 0); } -function getRate(yLeft: AxisSide, yRight: AxisSide) { - let rateLeft, rateRight, rate; +function getRate(yLeft: AxisSide, yRight: AxisSide): number { if (checkTwoCross(yLeft, yRight)) { - rateLeft = yRight.min ? yLeft.min / yRight.min : 0; - rateRight = yRight.max ? yLeft.max / yRight.max : 0; - } else { - if (checkOneSide(yLeft, yRight)) { - const absLeftMin = Math.abs(yLeft.min); - const absLeftMax = Math.abs(yLeft.max); - const absRightMin = Math.abs(yRight.min); - const absRightMax = Math.abs(yRight.max); - const upLeft = _.max([absLeftMin, absLeftMax]); - const downLeft = _.min([absLeftMin, absLeftMax]); - const upRight = _.max([absRightMin, absRightMax]); - const downRight = _.min([absRightMin, absRightMax]); + const rateLeft = yRight.min ? yLeft.min / yRight.min : 0; + const rateRight = yRight.max ? yLeft.max / yRight.max : 0; - rateLeft = downLeft ? upLeft / downLeft : upLeft; - rateRight = downRight ? upRight / downRight : upRight; - } else { - if (yLeft.min > 0 || yRight.min > 0) { - rateLeft = yLeft.max / yRight.max; - rateRight = 0; - } else { - rateLeft = 0; - rateRight = yLeft.min / yRight.min; - } - } + return rateLeft > rateRight ? rateLeft : rateRight; } - rate = rateLeft > rateRight ? rateLeft : rateRight; + if (checkOneSide(yLeft, yRight)) { + const absLeftMin = Math.abs(yLeft.min); + const absLeftMax = Math.abs(yLeft.max); + const absRightMin = Math.abs(yRight.min); + const absRightMax = Math.abs(yRight.max); + const upLeft = _.max([absLeftMin, absLeftMax]); + const downLeft = _.min([absLeftMin, absLeftMax]); + const upRight = _.max([absRightMin, absRightMax]); + const downRight = _.min([absRightMin, absRightMax]); - return rate; + const rateLeft = downLeft ? upLeft / downLeft : upLeft; + const rateRight = downRight ? upRight / downRight : upRight; + + return rateLeft > rateRight ? rateLeft : rateRight; + } + + if (yLeft.min > 0 || yRight.min > 0) { + return yLeft.max / yRight.max; + } else { + return yLeft.min / yRight.min; + } } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index d0553425f7f..ab0169bb543 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -244,6 +244,13 @@ class GraphCtrl extends MetricsPanelCtrl { this.loading = false; this.alertState = result.alertState; this.annotations = result.annotations; + + // Temp alerting & react hack + // Add it to the seriesList so react can access it + if (this.alertState) { + (this.seriesList as any).alertState = this.alertState.state; + } + this.render(this.seriesList); }, () => { diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index c622f4167fa..c21fd2e87b2 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -55,6 +55,7 @@ describe('grafanaGraph', () => { panel: { events: { on: () => {}, + emit: () => {}, }, legend: {}, grid: {}, diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts index ec6532fb40c..ed4bcfbdd69 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts @@ -25,6 +25,7 @@ describe('GraphCtrl', () => { GraphCtrl.prototype.panel = { events: { on: () => {}, + emit: () => {}, }, gridPos: { w: 100, diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index efefa681a89..257d16d93cc 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -577,8 +577,8 @@ export class HeatmapRenderer { highlightCard(event: any) { const color = d3.select(event.target).style('fill'); - const highlightColor = d3.color(color).darker(2); - const strokeColor = d3.color(color).brighter(4); + const highlightColor = d3.color(color)!.darker(2); + const strokeColor = d3.color(color)!.brighter(4); const currentCard = d3.select(event.target); this.tooltip.originalFillColor = color; currentCard diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 6295f18baa6..d60d4771d31 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -348,9 +348,12 @@ class SingleStatCtrl extends MetricsPanelCtrl { const panel = ctrl.panel; const templateSrv = this.templateSrv; let linkInfo: LinkModel | null = null; - const $panelContainer = elem.find('.panel-container'); elem = elem.find('.singlestat-panel'); + function getPanelContainer() { + return elem.closest('.panel-container'); + } + function applyColoringThresholds(valueString: string) { const data = ctrl.data; const color = getColorForValue(data, data.value); @@ -588,18 +591,18 @@ class SingleStatCtrl extends MetricsPanelCtrl { if (panel.colorBackground) { const color = getColorForValue(data, data.display.numeric); if (color) { - $panelContainer.css('background-color', color); + getPanelContainer().css('background-color', color); if (scope.fullscreen) { elem.css('background-color', color); } else { elem.css('background-color', ''); } } else { - $panelContainer.css('background-color', ''); + getPanelContainer().css('background-color', ''); elem.css('background-color', ''); } } else { - $panelContainer.css('background-color', ''); + getPanelContainer().css('background-color', ''); elem.css('background-color', ''); } diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index 44f02ecc7c1..5d4ad33b7ef 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -26,23 +26,21 @@ describe('SingleStatCtrl', () => { const $sanitize = {}; - SingleStatCtrl.prototype.panel = { - events: { - on: () => {}, - emit: () => {}, - }, - }; SingleStatCtrl.prototype.dashboard = ({ getTimezone: jest.fn(() => 'utc'), } as any) as DashboardModel; - SingleStatCtrl.prototype.events = { - on: () => {}, - }; function singleStatScenario(desc: string, func: any) { describe(desc, () => { ctx.setup = (setupFunc: any) => { beforeEach(() => { + SingleStatCtrl.prototype.panel = { + events: { + on: () => {}, + emit: () => {}, + }, + }; + // @ts-ignore ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize); setupFunc(); @@ -335,9 +333,6 @@ describe('SingleStatCtrl', () => { singleStatScenario('with default values', (ctx: TestContext) => { ctx.setup(() => { ctx.input = tableData; - ctx.ctrl.panel = { - emit: () => {}, - }; ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.format = 'none'; }); @@ -386,7 +381,7 @@ describe('SingleStatCtrl', () => { ctx.setup(() => { ctx.input = tableData; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 10, 'ignore2']; - ctx.ctrl.panel.mappingType = 2; + ctx.ctrl.panel.mappingType = 1; ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; }); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index fa5fdd3c034..507332e837f 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -6,12 +6,11 @@ import { transformDataToTable } from './transformers'; import { tablePanelEditor } from './editor'; import { columnOptionsTab } from './column_options'; import { TableRenderer } from './renderer'; -import { isTableData } from '@grafana/data'; +import { isTableData, PanelEvents, PanelPlugin } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { PanelEvents } from '@grafana/data'; import { CoreEvents } from 'app/types'; -class TablePanelCtrl extends MetricsPanelCtrl { +export class TablePanelCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; pageIndex: number; @@ -280,4 +279,6 @@ class TablePanelCtrl extends MetricsPanelCtrl { } } -export { TablePanelCtrl, TablePanelCtrl as PanelCtrl }; +export const plugin = new PanelPlugin(null); +plugin.angularPanelCtrl = TablePanelCtrl; +plugin.setNoPadding(); diff --git a/public/sass/components/_panel_header.scss b/public/sass/components/_panel_header.scss index 6912f8e017f..9a9c1bf5b72 100644 --- a/public/sass/components/_panel_header.scss +++ b/public/sass/components/_panel_header.scss @@ -53,7 +53,7 @@ $panel-header-no-title-zindex: 1; } .panel-menu-container { - width: 1px; + width: 0px; height: 19px; display: inline-block; } diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index dbd76dcd3a1..bdf32b2da2e 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -63,7 +63,7 @@ } td { - padding: 0.45em 0 0.45em 1.1em; + padding: 0.45em 1.1em; border-bottom: 2px solid $body-bg; border-right: 2px solid $body-bg; diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 28756756aa5..3d031b7f308 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -28,7 +28,15 @@ border: 0; } +.panel-alert-icon { + display: none; +} + .panel-has-alert { + .panel-alert-icon { + display: inline-block; + } + .panel-alert-icon:before { content: '\e611'; position: relative; @@ -40,7 +48,6 @@ .panel-alert-state { &--alerting { box-shadow: 0 0 10px rgba($critical, 0.5); - position: relative; .panel-alert-icon:before { color: $critical;