mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelChrome: Use react Panel Header for angular panels. (#21265)
* WIP: Angular panel chrome, this is going to be tricky * AngularPanelChrome: initial render works * Options are showing up * viz options working * Fixed singlestat background * AngularPanels: Fixed alert tab * Removed anuglar loading spinner * Dashboard: Refactor dashboard reducer & actions * Dashboard: minor refactoring * PanelChrome: loading state moved to header * Subscribe to render events to solve title update issue * Time info and query errors now works * PanelHeader: unifying angular and react behavior * added getPanelMenu test * Scrollable now works again * Various fixes * Making stuff work * seperate event emitter for angular * Fixed issue sending updated dimensions to angular panel * Minor tweaks * Fixed tests * Alerting: alert state now works * Fixed unit tests * Fixed a few null check errors * Simplified events handling * Fixed strict null checks
This commit is contained in:
@@ -136,6 +136,7 @@ export interface PanelMenuItem {
|
||||
iconClassName?: string;
|
||||
onClick?: (event: React.MouseEvent<any>) => void;
|
||||
shortcut?: string;
|
||||
href?: string;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
|
@@ -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<DataQueryError>('data-error');
|
||||
export const dataReceived = eventFactory<DataQueryResponseData[]>('data-received');
|
||||
export const dataSnapshotLoad = eventFactory<DataQueryResponseData[]>('data-snapshot-load');
|
||||
export const editModeInitialized = eventFactory('init-edit-mode');
|
||||
export const initPanelActions = eventFactory<MenuElement[]>('init-panel-actions');
|
||||
export const initPanelActions = eventFactory<AngularPanelMenuItem[]>('init-panel-actions');
|
||||
export const panelChangeView = eventFactory<PanelChangeViewPayload>('panel-change-view');
|
||||
export const panelInitialized = eventFactory('panel-initialized');
|
||||
export const panelSizeChanged = eventFactory('panel-size-changed');
|
||||
|
@@ -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<Props, State> {
|
||||
};
|
||||
|
||||
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<Props, State> {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@@ -90,6 +90,7 @@ export class AnnotationsSrv {
|
||||
this.alertStatesPromise = getBackendSrv().get('/api/alerts/states-for-dashboard', {
|
||||
dashboardId: options.dashboard.id,
|
||||
});
|
||||
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
|
@@ -183,6 +183,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
try {
|
||||
this.props.dashboard.render();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notifyApp(createErrorNotification(`Panel rendering error`, err));
|
||||
}
|
||||
}
|
||||
|
@@ -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<Props, State> {
|
||||
|
||||
this.state = {
|
||||
plugin: null,
|
||||
angularPanel: null,
|
||||
isLazy: !props.isInView,
|
||||
};
|
||||
|
||||
@@ -77,16 +75,13 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
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<Props, State> {
|
||||
if (this.state.isLazy && this.props.isInView) {
|
||||
this.setState({ isLazy: false });
|
||||
}
|
||||
|
||||
if (!this.element || this.state.angularPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = getAngularLoader();
|
||||
const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
|
||||
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<Props, State> {
|
||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
if (plugin.angularPanelCtrl) {
|
||||
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
@@ -145,6 +114,20 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (plugin.angularPanelCtrl) {
|
||||
return (
|
||||
<PanelChromeAngular
|
||||
plugin={plugin}
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isFullscreen={isFullscreen}
|
||||
isInView={isInView}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelChrome
|
||||
plugin={plugin}
|
||||
@@ -164,7 +147,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
|
||||
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<Props, State> {
|
||||
panel={panel}
|
||||
plugin={plugin}
|
||||
dashboard={dashboard}
|
||||
angularPanel={angularPanel}
|
||||
onPluginTypeChange={this.onPluginTypeChange}
|
||||
/>
|
||||
)}
|
||||
|
@@ -272,7 +272,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
|
||||
// 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<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
<div className={panelContentClassNames}>
|
||||
<PanelComponent
|
||||
id={panel.id}
|
||||
@@ -311,14 +310,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private renderLoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
hasOverlayHeader() {
|
||||
const { panel } = this.props;
|
||||
const { errorMessage, data } = this.state;
|
||||
@@ -360,6 +351,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
isLoading={data.state === LoadingState.Loading}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
|
212
public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx
Normal file
212
public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx
Normal file
@@ -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<Props, State> {
|
||||
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 = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
|
||||
|
||||
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 (
|
||||
<div className={containerClassNames}>
|
||||
<PanelHeader
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
timeInfo={data.request ? data.request.timeInfo : null}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
isLoading={data.state === LoadingState.Loading}
|
||||
/>
|
||||
<div className={panelContentClassNames}>
|
||||
<div ref={element => (this.element = element)} className="panel-height-helper" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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<Props, State> {
|
||||
clickCoordinates: ClickCoordinates = { x: 0, y: 0 };
|
||||
state = {
|
||||
|
||||
state: State = {
|
||||
panelMenuOpen: false,
|
||||
clickCoordinates: { x: 0, y: 0 },
|
||||
menuItems: [],
|
||||
};
|
||||
|
||||
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -57,13 +61,19 @@ export class PanelHeader extends Component<Props, State> {
|
||||
};
|
||||
|
||||
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
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<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
private renderLoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && this.renderLoadingState()}
|
||||
<div className={panelHeaderClass}>
|
||||
<PanelHeaderCorner
|
||||
panel={panel}
|
||||
@@ -105,7 +125,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
</span>
|
||||
{this.state.panelMenuOpen && (
|
||||
<ClickOutsideWrapper onClick={this.closeMenu}>
|
||||
<PanelHeaderMenu panel={panel} dashboard={dashboard} />
|
||||
<PanelHeaderMenu items={menuItems} />
|
||||
</ClickOutsideWrapper>
|
||||
)}
|
||||
{timeInfo && (
|
||||
|
@@ -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<Props> {
|
||||
@@ -33,9 +29,6 @@ export class PanelHeaderMenu extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel } = this.props;
|
||||
const menu = getPanelMenu(dashboard, panel);
|
||||
|
||||
return <div className="panel-menu-container dropdown open">{this.renderItems(menu)}</div>;
|
||||
return <div className="panel-menu-container dropdown open">{this.renderItems(this.props.items)}</div>;
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
|
||||
<li className="divider" />
|
||||
) : (
|
||||
<li className={isSubMenu ? 'dropdown-submenu' : null}>
|
||||
<a onClick={props.onClick}>
|
||||
<a onClick={props.onClick} href={props.href}>
|
||||
{props.iconClassName && <i className={props.iconClassName} />}
|
||||
<span
|
||||
className="dropdown-item-text"
|
||||
|
@@ -0,0 +1,103 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
||||
// Types
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
import { angularPanelUpdated } from '../state/PanelModel';
|
||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
import { PanelCtrl } from 'app/plugins/sdk';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
||||
}
|
||||
|
||||
export class AngularPanelOptions extends PureComponent<Props> {
|
||||
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 +=
|
||||
`
|
||||
<div class="panel-options-group" ng-cloak>` +
|
||||
(i > 0
|
||||
? `<div class="panel-options-group__header">
|
||||
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
|
||||
</span>
|
||||
</div>`
|
||||
: '') +
|
||||
`<div class="panel-options-group__body">
|
||||
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const loader = getAngularLoader();
|
||||
const scopeProps = { ctrl: panelCtrl };
|
||||
|
||||
this.angularOptions = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={elem => (this.element = elem)} />;
|
||||
}
|
||||
}
|
@@ -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<PanelEditorProps> {
|
||||
};
|
||||
|
||||
renderCurrentTab(activeTab: string) {
|
||||
const { panel, dashboard, plugin, angularPanel } = this.props;
|
||||
|
||||
const { panel, dashboard, plugin } = this.props;
|
||||
switch (activeTab) {
|
||||
case 'advanced':
|
||||
return <GeneralTab panel={panel} />;
|
||||
case 'queries':
|
||||
return <QueriesTab panel={panel} dashboard={dashboard} />;
|
||||
case 'alert':
|
||||
return <AlertTab angularPanel={angularPanel} dashboard={dashboard} panel={panel} />;
|
||||
return <AlertTab dashboard={dashboard} panel={panel} />;
|
||||
case 'visualization':
|
||||
return (
|
||||
<VisualizationTab
|
||||
@@ -87,7 +85,6 @@ class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
||||
dashboard={dashboard}
|
||||
plugin={plugin}
|
||||
onPluginTypeChange={this.onPluginTypeChange}
|
||||
angularPanel={angularPanel}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
@@ -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<Props, State> {
|
||||
};
|
||||
|
||||
renderPanelOptions() {
|
||||
const { plugin, angularPanel } = this.props;
|
||||
const { plugin, dashboard, panel } = this.props;
|
||||
|
||||
if (angularPanel) {
|
||||
return <div ref={element => (this.element = element)} />;
|
||||
if (plugin.angularPanelCtrl) {
|
||||
return (
|
||||
<AngularPanelOptions
|
||||
plugin={plugin}
|
||||
dashboard={dashboard}
|
||||
panel={panel}
|
||||
onPluginTypeChange={this.onPluginTypeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin.editor) {
|
||||
@@ -85,82 +91,16 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
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 +=
|
||||
`
|
||||
<div class="panel-options-group" ng-cloak>` +
|
||||
(i > 0
|
||||
? `<div class="panel-options-group__header">
|
||||
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
|
||||
</span>
|
||||
</div>`
|
||||
: '') +
|
||||
`<div class="panel-options-group__body">
|
||||
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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<PanelModel | undefined>('panel-added');
|
||||
export const panelRemoved = eventFactory<PanelModel | undefined>('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 {
|
||||
|
@@ -26,13 +26,13 @@ const dashbardSlice = createSlice({
|
||||
loadDashboardPermissions: (state, action: PayloadAction<DashboardAclDTO[]>) => {
|
||||
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<MutableDashboard>) => {
|
||||
@@ -41,16 +41,13 @@ const dashbardSlice = createSlice({
|
||||
state.isInitSlow = false;
|
||||
},
|
||||
dashboardInitFailed: (state, action: PayloadAction<DashboardInitError>) => {
|
||||
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<QueriesToUpdateOnDashboardLoad>) => {
|
||||
state.modifiedQueries = action.payload;
|
||||
},
|
||||
clearDashboardQueriesToUpdateOnLoad: state => {
|
||||
clearDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction) => {
|
||||
state.modifiedQueries = null;
|
||||
},
|
||||
},
|
||||
|
63
public/app/features/dashboard/utils/getPanelMenu.test.ts
Normal file
63
public/app/features/dashboard/utils/getPanelMenu.test.ts
Normal file
@@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
@@ -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<any>) => {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import './panel_header';
|
||||
import './panel_directive';
|
||||
import './query_ctrl';
|
||||
import './panel_editor_tab';
|
||||
|
@@ -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 };
|
||||
|
@@ -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 = '<div class="markdown-html panel-info-content">';
|
||||
|
||||
const md = renderMarkdown(interpolatedMarkdown);
|
||||
html += md;
|
||||
|
||||
if (panel.links && panel.links.length > 0) {
|
||||
const interpolatedLinks = getPanelLinksSupplier(panel).getLinks();
|
||||
html += '<ul class="panel-info-corner-links">';
|
||||
for (const link of interpolatedLinks) {
|
||||
html +=
|
||||
'<li><a class="panel-menu-link" href="' +
|
||||
escapeHtml(link.href) +
|
||||
'" target="' +
|
||||
escapeHtml(link.target) +
|
||||
'">' +
|
||||
escapeHtml(link.title) +
|
||||
'</a></li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return config.disableSanitizeHtml ? html : sanitize(html);
|
||||
}
|
||||
|
||||
// overriden from react
|
||||
onPluginTypeChange = (plugin: PanelPluginMeta) => {};
|
||||
}
|
||||
|
@@ -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 = `
|
||||
<div class="panel-container" ng-class="{'panel-container--no-title': !ctrl.panel.title.length}">
|
||||
<div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.panel.fullscreen}">
|
||||
<span class="panel-info-corner">
|
||||
<i class="fa"></i>
|
||||
<span class="panel-info-corner-inner"></span>
|
||||
</span>
|
||||
|
||||
<span class="panel-loading" ng-show="ctrl.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
|
||||
<panel-header class="panel-title-container" panel-ctrl="ctrl" aria-label={{ctrl.selectors.title(ctrl.panel.title)}}></panel-header>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<ng-transclude class="panel-height-helper"></ng-transclude>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-transclude class="panel-height-helper"></ng-transclude>
|
||||
`;
|
||||
|
||||
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) => {
|
||||
</div>
|
||||
`;
|
||||
|
||||
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: `
|
||||
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
|
||||
<span data-placement="top" bs-tooltip="ctrl.error">
|
||||
<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
|
||||
</span>
|
||||
</span>
|
||||
`,
|
||||
link: (scope, elem) => {},
|
||||
};
|
||||
});
|
||||
|
@@ -1,123 +0,0 @@
|
||||
import { coreModule } from 'app/core/core';
|
||||
import { AngularPanelMenuItem } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
const template = `
|
||||
<span class="panel-title">
|
||||
<span class="icon-gf panel-alert-icon"></span>
|
||||
<span class="panel-title-text">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>
|
||||
<span class="panel-menu-container dropdown">
|
||||
<span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
|
||||
<ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
|
||||
</ul>
|
||||
</span>
|
||||
<span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
|
||||
</span>`;
|
||||
|
||||
function renderMenuItem(item: AngularPanelMenuItem, ctrl: any) {
|
||||
let html = '';
|
||||
let listItemClass = '';
|
||||
|
||||
if (item.divider) {
|
||||
return '<li class="divider"></li>';
|
||||
}
|
||||
|
||||
if (item.submenu) {
|
||||
listItemClass = 'dropdown-submenu';
|
||||
}
|
||||
|
||||
html += `<li class="${listItemClass}"><a `;
|
||||
|
||||
if (item.click) {
|
||||
html += ` ng-click="${item.click}"`;
|
||||
}
|
||||
if (item.href) {
|
||||
html += ` href="${item.href}"`;
|
||||
}
|
||||
|
||||
html += `><i class="${item.icon}"></i>`;
|
||||
html += `<span class="dropdown-item-text" aria-label="${e2e.pages.Dashboard.Panels.Panel.selectors.headerItems(
|
||||
item.text
|
||||
)}">${item.text}</span>`;
|
||||
|
||||
if (item.shortcut) {
|
||||
html += `<span class="dropdown-menu-item-shortcut">${item.shortcut}</span>`;
|
||||
}
|
||||
|
||||
html += `</a>`;
|
||||
|
||||
if (item.submenu) {
|
||||
html += '<ul class="dropdown-menu dropdown-menu--menu panel-menu">';
|
||||
for (const subitem of item.submenu) {
|
||||
html += renderMenuItem(subitem, ctrl);
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
html += `</li>`;
|
||||
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);
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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] });
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
},
|
||||
() => {
|
||||
|
@@ -55,6 +55,7 @@ describe('grafanaGraph', () => {
|
||||
panel: {
|
||||
events: {
|
||||
on: () => {},
|
||||
emit: () => {},
|
||||
},
|
||||
legend: {},
|
||||
grid: {},
|
||||
|
@@ -25,6 +25,7 @@ describe('GraphCtrl', () => {
|
||||
GraphCtrl.prototype.panel = {
|
||||
events: {
|
||||
on: () => {},
|
||||
emit: () => {},
|
||||
},
|
||||
gridPos: {
|
||||
w: 100,
|
||||
|
@@ -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
|
||||
|
@@ -348,9 +348,12 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
const panel = ctrl.panel;
|
||||
const templateSrv = this.templateSrv;
|
||||
let linkInfo: LinkModel<any> | 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', '');
|
||||
}
|
||||
|
||||
|
@@ -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' }];
|
||||
});
|
||||
|
@@ -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();
|
||||
|
@@ -53,7 +53,7 @@ $panel-header-no-title-zindex: 1;
|
||||
}
|
||||
|
||||
.panel-menu-container {
|
||||
width: 1px;
|
||||
width: 0px;
|
||||
height: 19px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user