mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05: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;
|
iconClassName?: string;
|
||||||
onClick?: (event: React.MouseEvent<any>) => void;
|
onClick?: (event: React.MouseEvent<any>) => void;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
|
href?: string;
|
||||||
subMenu?: PanelMenuItem[];
|
subMenu?: PanelMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { eventFactory } from './utils';
|
import { eventFactory } from './utils';
|
||||||
import { DataQueryError, DataQueryResponseData } from './datasource';
|
import { DataQueryError, DataQueryResponseData } from './datasource';
|
||||||
|
import { AngularPanelMenuItem } from './panel';
|
||||||
|
|
||||||
/** Payloads */
|
/** Payloads */
|
||||||
export interface PanelChangeViewPayload {
|
export interface PanelChangeViewPayload {
|
||||||
@@ -9,13 +10,6 @@ export interface PanelChangeViewPayload {
|
|||||||
toggle?: boolean;
|
toggle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuElement {
|
|
||||||
text: string;
|
|
||||||
click: string;
|
|
||||||
role?: string;
|
|
||||||
shortcut?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Events */
|
/** Events */
|
||||||
|
|
||||||
export const refresh = eventFactory('refresh');
|
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 dataReceived = eventFactory<DataQueryResponseData[]>('data-received');
|
||||||
export const dataSnapshotLoad = eventFactory<DataQueryResponseData[]>('data-snapshot-load');
|
export const dataSnapshotLoad = eventFactory<DataQueryResponseData[]>('data-snapshot-load');
|
||||||
export const editModeInitialized = eventFactory('init-edit-mode');
|
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 panelChangeView = eventFactory<PanelChangeViewPayload>('panel-change-view');
|
||||||
export const panelInitialized = eventFactory('panel-initialized');
|
export const panelInitialized = eventFactory('panel-initialized');
|
||||||
export const panelSizeChanged = eventFactory('panel-size-changed');
|
export const panelSizeChanged = eventFactory('panel-size-changed');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import StateHistory from './StateHistory';
|
|||||||
import 'app/features/alerting/AlertTabCtrl';
|
import 'app/features/alerting/AlertTabCtrl';
|
||||||
|
|
||||||
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
||||||
import { PanelModel } from '../dashboard/state/PanelModel';
|
import { PanelModel, angularPanelUpdated } from '../dashboard/state/PanelModel';
|
||||||
import { TestRuleResult } from './TestRuleResult';
|
import { TestRuleResult } from './TestRuleResult';
|
||||||
import { AppNotificationSeverity, StoreState } from 'app/types';
|
import { AppNotificationSeverity, StoreState } from 'app/types';
|
||||||
import { PanelEditorTabIds, getPanelEditorTab } from '../dashboard/panel_editor/state/reducers';
|
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';
|
import { CoreEvents } from 'app/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
angularPanel?: AngularComponent;
|
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
changePanelEditorTab: typeof changePanelEditorTab;
|
changePanelEditorTab: typeof changePanelEditorTab;
|
||||||
@@ -42,19 +41,16 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.shouldLoadAlertTab()) {
|
this.loadAlertTab();
|
||||||
this.loadAlertTab();
|
this.props.panel.events.on(angularPanelUpdated, this.onAngularPanelUpdated);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAngularPanelUpdated = () => {
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
if (this.shouldLoadAlertTab()) {
|
this.loadAlertTab();
|
||||||
this.loadAlertTab();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldLoadAlertTab() {
|
|
||||||
return this.props.angularPanel && this.element && !this.component;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -64,9 +60,13 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadAlertTab() {
|
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
|
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||||
if (!scope.$$childHead) {
|
if (!scope.$$childHead) {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export class AnnotationsSrv {
|
|||||||
this.alertStatesPromise = getBackendSrv().get('/api/alerts/states-for-dashboard', {
|
this.alertStatesPromise = getBackendSrv().get('/api/alerts/states-for-dashboard', {
|
||||||
dashboardId: options.dashboard.id,
|
dashboardId: options.dashboard.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.alertStatesPromise;
|
return this.alertStatesPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
|||||||
try {
|
try {
|
||||||
this.props.dashboard.render();
|
this.props.dashboard.render();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
this.props.notifyApp(createErrorNotification(`Panel rendering 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';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
import { getAngularLoader, AngularComponent } from '@grafana/runtime';
|
|
||||||
import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
|
import { importPanelPlugin } from 'app/features/plugins/plugin_loader';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@@ -13,6 +12,7 @@ import { DashboardRow } from '../components/DashboardRow';
|
|||||||
import { PanelChrome } from './PanelChrome';
|
import { PanelChrome } from './PanelChrome';
|
||||||
import { PanelEditor } from '../panel_editor/PanelEditor';
|
import { PanelEditor } from '../panel_editor/PanelEditor';
|
||||||
import { PanelResizer } from './PanelResizer';
|
import { PanelResizer } from './PanelResizer';
|
||||||
|
import { PanelChromeAngular } from './PanelChromeAngular';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PanelModel, DashboardModel } from '../state';
|
import { PanelModel, DashboardModel } from '../state';
|
||||||
@@ -29,7 +29,6 @@ export interface Props {
|
|||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
angularPanel: AngularComponent;
|
|
||||||
isLazy: boolean;
|
isLazy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
plugin: null,
|
plugin: null,
|
||||||
angularPanel: null,
|
|
||||||
isLazy: !props.isInView,
|
isLazy: !props.isInView,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,16 +75,13 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
if (!this.state.plugin || this.state.plugin.meta.id !== pluginId) {
|
if (!this.state.plugin || this.state.plugin.meta.id !== pluginId) {
|
||||||
const plugin = await importPanelPlugin(pluginId);
|
const plugin = await importPanelPlugin(pluginId);
|
||||||
|
|
||||||
// unmount angular panel
|
|
||||||
this.cleanUpAngularPanel();
|
|
||||||
|
|
||||||
if (panel.type !== pluginId) {
|
if (panel.type !== pluginId) {
|
||||||
panel.changePlugin(plugin);
|
panel.changePlugin(plugin);
|
||||||
} else {
|
} else {
|
||||||
panel.pluginLoaded(plugin);
|
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) {
|
if (this.state.isLazy && this.props.isInView) {
|
||||||
this.setState({ isLazy: false });
|
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 = () => {
|
onMouseEnter = () => {
|
||||||
@@ -134,10 +107,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props;
|
const { dashboard, panel, isFullscreen, isInView, isInEditMode } = this.props;
|
||||||
const { plugin } = this.state;
|
const { plugin } = this.state;
|
||||||
|
|
||||||
if (plugin.angularPanelCtrl) {
|
|
||||||
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ width, height }) => {
|
{({ width, height }) => {
|
||||||
@@ -145,6 +114,20 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (plugin.angularPanelCtrl) {
|
||||||
|
return (
|
||||||
|
<PanelChromeAngular
|
||||||
|
plugin={plugin}
|
||||||
|
panel={panel}
|
||||||
|
dashboard={dashboard}
|
||||||
|
isFullscreen={isFullscreen}
|
||||||
|
isInView={isInView}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelChrome
|
<PanelChrome
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
@@ -164,7 +147,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { panel, dashboard, isFullscreen, isEditing } = this.props;
|
const { panel, dashboard, isFullscreen, isEditing } = this.props;
|
||||||
const { plugin, angularPanel, isLazy } = this.state;
|
const { plugin, isLazy } = this.state;
|
||||||
|
|
||||||
if (this.isSpecial(panel.type)) {
|
if (this.isSpecial(panel.type)) {
|
||||||
return this.specialPanels[panel.type]();
|
return this.specialPanels[panel.type]();
|
||||||
@@ -212,7 +195,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
panel={panel}
|
panel={panel}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
angularPanel={angularPanel}
|
|
||||||
onPluginTypeChange={this.onPluginTypeChange}
|
onPluginTypeChange={this.onPluginTypeChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
// do not render component until we have first data
|
// do not render component until we have first data
|
||||||
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
||||||
return this.renderLoadingState();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PanelComponent = plugin.panel;
|
const PanelComponent = plugin.panel;
|
||||||
@@ -290,7 +290,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
|
||||||
<div className={panelContentClassNames}>
|
<div className={panelContentClassNames}>
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
id={panel.id}
|
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() {
|
hasOverlayHeader() {
|
||||||
const { panel } = this.props;
|
const { panel } = this.props;
|
||||||
const { errorMessage, data } = this.state;
|
const { errorMessage, data } = this.state;
|
||||||
@@ -360,6 +351,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
links={panel.links}
|
links={panel.links}
|
||||||
error={errorMessage}
|
error={errorMessage}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
|
isLoading={data.state === LoadingState.Loading}
|
||||||
/>
|
/>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{({ error, errorInfo }) => {
|
{({ error, errorInfo }) => {
|
||||||
|
|||||||
@@ -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 React, { Component } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { DataLink, ScopedVars } from '@grafana/data';
|
import { DataLink, ScopedVars, PanelMenuItem } from '@grafana/data';
|
||||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||||
import { e2e } from '@grafana/e2e';
|
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 { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
|
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -23,6 +24,7 @@ export interface Props {
|
|||||||
links?: DataLink[];
|
links?: DataLink[];
|
||||||
error?: string;
|
error?: string;
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClickCoordinates {
|
interface ClickCoordinates {
|
||||||
@@ -32,13 +34,15 @@ interface ClickCoordinates {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
panelMenuOpen: boolean;
|
panelMenuOpen: boolean;
|
||||||
|
menuItems: PanelMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelHeader extends Component<Props, State> {
|
export class PanelHeader extends Component<Props, State> {
|
||||||
clickCoordinates: ClickCoordinates = { x: 0, y: 0 };
|
clickCoordinates: ClickCoordinates = { x: 0, y: 0 };
|
||||||
state = {
|
|
||||||
|
state: State = {
|
||||||
panelMenuOpen: false,
|
panelMenuOpen: false,
|
||||||
clickCoordinates: { x: 0, y: 0 },
|
menuItems: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
|
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
@@ -57,13 +61,19 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (this.isClick(this.eventToClickCoordinates(event))) {
|
if (!this.isClick(this.eventToClickCoordinates(event))) {
|
||||||
event.stopPropagation();
|
return;
|
||||||
|
|
||||||
this.setState(prevState => ({
|
|
||||||
panelMenuOpen: !prevState.panelMenuOpen,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { dashboard, panel } = this.props;
|
||||||
|
const menuItems = getPanelMenu(dashboard, panel);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
panelMenuOpen: !this.state.panelMenuOpen,
|
||||||
|
menuItems,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
closeMenu = () => {
|
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() {
|
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 title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||||
|
|
||||||
const panelHeaderClass = classNames({
|
const panelHeaderClass = classNames({
|
||||||
@@ -83,6 +102,7 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isLoading && this.renderLoadingState()}
|
||||||
<div className={panelHeaderClass}>
|
<div className={panelHeaderClass}>
|
||||||
<PanelHeaderCorner
|
<PanelHeaderCorner
|
||||||
panel={panel}
|
panel={panel}
|
||||||
@@ -105,7 +125,7 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
</span>
|
</span>
|
||||||
{this.state.panelMenuOpen && (
|
{this.state.panelMenuOpen && (
|
||||||
<ClickOutsideWrapper onClick={this.closeMenu}>
|
<ClickOutsideWrapper onClick={this.closeMenu}>
|
||||||
<PanelHeaderMenu panel={panel} dashboard={dashboard} />
|
<PanelHeaderMenu items={menuItems} />
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
)}
|
)}
|
||||||
{timeInfo && (
|
{timeInfo && (
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react';
|
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 { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
|
||||||
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
|
||||||
import { PanelMenuItem } from '@grafana/data';
|
import { PanelMenuItem } from '@grafana/data';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
items: PanelMenuItem[];
|
||||||
dashboard: DashboardModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelHeaderMenu extends PureComponent<Props> {
|
export class PanelHeaderMenu extends PureComponent<Props> {
|
||||||
@@ -33,9 +29,6 @@ export class PanelHeaderMenu extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, panel } = this.props;
|
return <div className="panel-menu-container dropdown open">{this.renderItems(this.props.items)}</div>;
|
||||||
const menu = getPanelMenu(dashboard, panel);
|
|
||||||
|
|
||||||
return <div className="panel-menu-container dropdown open">{this.renderItems(menu)}</div>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
|
|||||||
<li className="divider" />
|
<li className="divider" />
|
||||||
) : (
|
) : (
|
||||||
<li className={isSubMenu ? 'dropdown-submenu' : null}>
|
<li className={isSubMenu ? 'dropdown-submenu' : null}>
|
||||||
<a onClick={props.onClick}>
|
<a onClick={props.onClick} href={props.href}>
|
||||||
{props.iconClassName && <i className={props.iconClassName} />}
|
{props.iconClassName && <i className={props.iconClassName} />}
|
||||||
<span
|
<span
|
||||||
className="dropdown-item-text"
|
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 { connect } from 'react-redux';
|
||||||
import { Tooltip } from '@grafana/ui';
|
import { Tooltip } from '@grafana/ui';
|
||||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||||
import { AngularComponent, config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
import { QueriesTab } from './QueriesTab';
|
import { QueriesTab } from './QueriesTab';
|
||||||
@@ -22,7 +22,6 @@ interface PanelEditorProps {
|
|||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
angularPanel?: AngularComponent;
|
|
||||||
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
||||||
activeTab: PanelEditorTabIds;
|
activeTab: PanelEditorTabIds;
|
||||||
tabs: PanelEditorTab[];
|
tabs: PanelEditorTab[];
|
||||||
@@ -71,15 +70,14 @@ class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderCurrentTab(activeTab: string) {
|
renderCurrentTab(activeTab: string) {
|
||||||
const { panel, dashboard, plugin, angularPanel } = this.props;
|
const { panel, dashboard, plugin } = this.props;
|
||||||
|
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'advanced':
|
case 'advanced':
|
||||||
return <GeneralTab panel={panel} />;
|
return <GeneralTab panel={panel} />;
|
||||||
case 'queries':
|
case 'queries':
|
||||||
return <QueriesTab panel={panel} dashboard={dashboard} />;
|
return <QueriesTab panel={panel} dashboard={dashboard} />;
|
||||||
case 'alert':
|
case 'alert':
|
||||||
return <AlertTab angularPanel={angularPanel} dashboard={dashboard} panel={panel} />;
|
return <AlertTab dashboard={dashboard} panel={panel} />;
|
||||||
case 'visualization':
|
case 'visualization':
|
||||||
return (
|
return (
|
||||||
<VisualizationTab
|
<VisualizationTab
|
||||||
@@ -87,7 +85,6 @@ class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
|
|||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
onPluginTypeChange={this.onPluginTypeChange}
|
onPluginTypeChange={this.onPluginTypeChange}
|
||||||
angularPanel={angularPanel}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
import { AngularComponent } from '@grafana/runtime';
|
||||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
import { connect } from 'react-redux';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { updateLocation } from 'app/core/actions';
|
import { updateLocation } from 'app/core/actions';
|
||||||
// Components
|
// Components
|
||||||
@@ -10,11 +10,11 @@ import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
|
|||||||
import { VizTypePicker } from './VizTypePicker';
|
import { VizTypePicker } from './VizTypePicker';
|
||||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||||
|
import { AngularPanelOptions } from './AngularPanelOptions';
|
||||||
// Types
|
// Types
|
||||||
import { PanelModel, DashboardModel } from '../state';
|
import { PanelModel, DashboardModel } from '../state';
|
||||||
import { VizPickerSearch } from './VizPickerSearch';
|
import { VizPickerSearch } from './VizPickerSearch';
|
||||||
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
||||||
import { PanelCtrl } from 'app/plugins/sdk';
|
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { PanelPlugin, PanelPluginMeta, PanelData, LoadingState, DefaultTimeRange } from '@grafana/data';
|
import { PanelPlugin, PanelPluginMeta, PanelData, LoadingState, DefaultTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ interface Props {
|
|||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
plugin: PanelPlugin;
|
plugin: PanelPlugin;
|
||||||
angularPanel?: AngularComponent;
|
|
||||||
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
||||||
updateLocation: typeof updateLocation;
|
updateLocation: typeof updateLocation;
|
||||||
urlOpenVizPicker: boolean;
|
urlOpenVizPicker: boolean;
|
||||||
@@ -63,10 +62,17 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderPanelOptions() {
|
renderPanelOptions() {
|
||||||
const { plugin, angularPanel } = this.props;
|
const { plugin, dashboard, panel } = this.props;
|
||||||
|
|
||||||
if (angularPanel) {
|
if (plugin.angularPanelCtrl) {
|
||||||
return <div ref={element => (this.element = element)} />;
|
return (
|
||||||
|
<AngularPanelOptions
|
||||||
|
plugin={plugin}
|
||||||
|
dashboard={dashboard}
|
||||||
|
panel={panel}
|
||||||
|
onPluginTypeChange={this.onPluginTypeChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.editor) {
|
if (plugin.editor) {
|
||||||
@@ -85,82 +91,16 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { panel } = this.props;
|
const { panel } = this.props;
|
||||||
const queryRunner = panel.getQueryRunner();
|
const queryRunner = panel.getQueryRunner();
|
||||||
if (this.shouldLoadAngularOptions()) {
|
|
||||||
this.loadAngularOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.querySubscription = queryRunner.getData().subscribe({
|
this.querySubscription = queryRunner.getData().subscribe({
|
||||||
next: (data: PanelData) => this.setState({ data }),
|
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() {
|
componentWillUnmount() {
|
||||||
if (this.querySubscription) {
|
if (this.querySubscription) {
|
||||||
this.querySubscription.unsubscribe();
|
this.querySubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
this.cleanUpAngularOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanUpAngularOptions() {
|
|
||||||
if (this.angularOptions) {
|
|
||||||
this.angularOptions.destroy();
|
|
||||||
this.angularOptions = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearQuery = () => {
|
clearQuery = () => {
|
||||||
@@ -276,4 +216,4 @@ const mapDispatchToProps = {
|
|||||||
updateLocation,
|
updateLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps);
|
export default connect(mapStateToProps, mapDispatchToProps)(VisualizationTab);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { PanelModel } from './PanelModel';
|
import { PanelModel } from './PanelModel';
|
||||||
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
|
import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks';
|
||||||
import { PanelEvents } from '@grafana/data';
|
|
||||||
|
|
||||||
class TablePanelCtrl {}
|
class TablePanelCtrl {}
|
||||||
|
|
||||||
@@ -148,18 +147,19 @@ describe('PanelModel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when changing from angular panel', () => {
|
describe('when changing from angular panel', () => {
|
||||||
let tearDownPublished = false;
|
const angularPanel = {
|
||||||
|
scope: {},
|
||||||
|
destroy: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
model.events.on(PanelEvents.panelTeardown, () => {
|
model.angularPanel = angularPanel;
|
||||||
tearDownPublished = true;
|
|
||||||
});
|
|
||||||
model.changePlugin(getPanelPlugin({ id: 'graph' }));
|
model.changePlugin(getPanelPlugin({ id: 'graph' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should teardown / destroy panel so angular panels event subscriptions are removed', () => {
|
it('should set angularPanel to undefined and call destory', () => {
|
||||||
expect(tearDownPublished).toBe(true);
|
expect(angularPanel.destroy.mock.calls.length).toBe(1);
|
||||||
expect(model.events.getEventCount()).toBe(0);
|
expect(model.angularPanel).toBe(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
DataTransformerConfig,
|
DataTransformerConfig,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { AngularComponent } from '@grafana/runtime';
|
||||||
|
|
||||||
import config from 'app/core/config';
|
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 panelAdded = eventFactory<PanelModel | undefined>('panel-added');
|
||||||
export const panelRemoved = eventFactory<PanelModel | undefined>('panel-removed');
|
export const panelRemoved = eventFactory<PanelModel | undefined>('panel-removed');
|
||||||
|
export const angularPanelUpdated = eventFactory('panel-angular-panel-updated');
|
||||||
|
|
||||||
export interface GridPos {
|
export interface GridPos {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -41,6 +43,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
|||||||
cachedPluginOptions: true,
|
cachedPluginOptions: true,
|
||||||
plugin: true,
|
plugin: true,
|
||||||
queryRunner: true,
|
queryRunner: true,
|
||||||
|
angularPanel: true,
|
||||||
restoreModel: true,
|
restoreModel: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,6 +138,8 @@ export class PanelModel {
|
|||||||
cachedPluginOptions?: any;
|
cachedPluginOptions?: any;
|
||||||
legend?: { show: boolean };
|
legend?: { show: boolean };
|
||||||
plugin?: PanelPlugin;
|
plugin?: PanelPlugin;
|
||||||
|
angularPanel?: AngularComponent;
|
||||||
|
|
||||||
private queryRunner?: PanelQueryRunner;
|
private queryRunner?: PanelQueryRunner;
|
||||||
|
|
||||||
constructor(model: any) {
|
constructor(model: any) {
|
||||||
@@ -290,9 +295,8 @@ export class PanelModel {
|
|||||||
const oldPluginId = this.type;
|
const oldPluginId = this.type;
|
||||||
const wasAngular = !!this.plugin.angularPanelCtrl;
|
const wasAngular = !!this.plugin.angularPanelCtrl;
|
||||||
|
|
||||||
// for angular panels we must remove all events and let angular panels do some cleanup
|
if (this.angularPanel) {
|
||||||
if (wasAngular) {
|
this.setAngularPanel(undefined);
|
||||||
this.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove panel type specific options
|
// remove panel type specific options
|
||||||
@@ -380,19 +384,32 @@ export class PanelModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.events.emit(PanelEvents.panelTeardown);
|
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
|
|
||||||
if (this.queryRunner) {
|
if (this.queryRunner) {
|
||||||
this.queryRunner.destroy();
|
this.queryRunner.destroy();
|
||||||
this.queryRunner = null;
|
this.queryRunner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.angularPanel) {
|
||||||
|
this.angularPanel.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTransformations(transformations: DataTransformerConfig[]) {
|
setTransformations(transformations: DataTransformerConfig[]) {
|
||||||
this.transformations = transformations;
|
this.transformations = transformations;
|
||||||
this.getQueryRunner().setTransformations(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 {
|
function getPluginVersion(plugin: PanelPlugin): string {
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ const dashbardSlice = createSlice({
|
|||||||
loadDashboardPermissions: (state, action: PayloadAction<DashboardAclDTO[]>) => {
|
loadDashboardPermissions: (state, action: PayloadAction<DashboardAclDTO[]>) => {
|
||||||
state.permissions = processAclItems(action.payload);
|
state.permissions = processAclItems(action.payload);
|
||||||
},
|
},
|
||||||
dashboardInitFetching: state => {
|
dashboardInitFetching: (state, action: PayloadAction) => {
|
||||||
state.initPhase = DashboardInitPhase.Fetching;
|
state.initPhase = DashboardInitPhase.Fetching;
|
||||||
},
|
},
|
||||||
dashboardInitServices: state => {
|
dashboardInitServices: (state, action: PayloadAction) => {
|
||||||
state.initPhase = DashboardInitPhase.Services;
|
state.initPhase = DashboardInitPhase.Services;
|
||||||
},
|
},
|
||||||
dashboardInitSlow: state => {
|
dashboardInitSlow: (state, action: PayloadAction) => {
|
||||||
state.isInitSlow = true;
|
state.isInitSlow = true;
|
||||||
},
|
},
|
||||||
dashboardInitCompleted: (state, action: PayloadAction<MutableDashboard>) => {
|
dashboardInitCompleted: (state, action: PayloadAction<MutableDashboard>) => {
|
||||||
@@ -41,16 +41,13 @@ const dashbardSlice = createSlice({
|
|||||||
state.isInitSlow = false;
|
state.isInitSlow = false;
|
||||||
},
|
},
|
||||||
dashboardInitFailed: (state, action: PayloadAction<DashboardInitError>) => {
|
dashboardInitFailed: (state, action: PayloadAction<DashboardInitError>) => {
|
||||||
const failedDashboard = new DashboardModel(
|
|
||||||
{ title: 'Dashboard init failed' },
|
|
||||||
{ canSave: false, canEdit: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
state.initPhase = DashboardInitPhase.Failed;
|
state.initPhase = DashboardInitPhase.Failed;
|
||||||
state.initError = action.payload;
|
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()) {
|
if (state.getModel()) {
|
||||||
state.getModel().destroy();
|
state.getModel().destroy();
|
||||||
state.getModel = () => null;
|
state.getModel = () => null;
|
||||||
@@ -63,7 +60,7 @@ const dashbardSlice = createSlice({
|
|||||||
setDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction<QueriesToUpdateOnDashboardLoad>) => {
|
setDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction<QueriesToUpdateOnDashboardLoad>) => {
|
||||||
state.modifiedQueries = action.payload;
|
state.modifiedQueries = action.payload;
|
||||||
},
|
},
|
||||||
clearDashboardQueriesToUpdateOnLoad: state => {
|
clearDashboardQueriesToUpdateOnLoad: (state, action: PayloadAction) => {
|
||||||
state.modifiedQueries = null;
|
state.modifiedQueries = null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 config from 'app/core/config';
|
||||||
import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime';
|
import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime';
|
||||||
import { PanelMenuItem } from '@grafana/data';
|
import { PanelMenuItem } from '@grafana/data';
|
||||||
|
|
||||||
import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
|
import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
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 { navigateToExplore } from '../../explore/state/actions';
|
||||||
import { getExploreUrl } from '../../../core/utils/explore';
|
import { getExploreUrl } from '../../../core/utils/explore';
|
||||||
import { getTimeSrv } from '../services/TimeSrv';
|
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>) => {
|
const onViewPanel = (event: React.MouseEvent<any>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
@@ -123,7 +123,7 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
|||||||
shortcut: 'p s',
|
shortcut: 'p s',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (contextSrv.hasAccessToExplore() && panel.datasource) {
|
if (contextSrv.hasAccessToExplore() && !panel.plugin.meta.skipDataQuery) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: 'Explore',
|
text: 'Explore',
|
||||||
iconClassName: 'gicon gicon-explore',
|
iconClassName: 'gicon gicon-explore',
|
||||||
@@ -170,6 +170,29 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
|||||||
onClick: onEditPanelJson,
|
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({
|
menu.push({
|
||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
text: 'More...',
|
text: 'More...',
|
||||||
@@ -190,4 +213,4 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return menu;
|
return menu;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import './panel_header';
|
|
||||||
import './panel_directive';
|
import './panel_directive';
|
||||||
import './query_ctrl';
|
import './query_ctrl';
|
||||||
import './panel_editor_tab';
|
import './panel_editor_tab';
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import kbn from 'app/core/utils/kbn';
|
|
||||||
|
|
||||||
import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
||||||
import { getExploreUrl } from 'app/core/utils/explore';
|
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||||
import { applyPanelTimeOverrides, getResolution } from 'app/features/dashboard/utils/panel';
|
|
||||||
import { ContextSrv } from 'app/core/services/context_srv';
|
import { ContextSrv } from 'app/core/services/context_srv';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
@@ -164,32 +160,14 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
updateTimeRange(datasource?: DataSourceApi) {
|
updateTimeRange(datasource?: DataSourceApi) {
|
||||||
this.datasource = datasource || this.datasource;
|
this.datasource = datasource || this.datasource;
|
||||||
this.range = this.timeSrv.timeRange();
|
this.range = this.timeSrv.timeRange();
|
||||||
this.resolution = getResolution(this.panel);
|
|
||||||
|
|
||||||
const newTimeData = applyPanelTimeOverrides(this.panel, this.range);
|
const newTimeData = applyPanelTimeOverrides(this.panel, this.range);
|
||||||
this.timeInfo = newTimeData.timeInfo;
|
this.timeInfo = newTimeData.timeInfo;
|
||||||
this.range = newTimeData.timeRange;
|
this.range = newTimeData.timeRange;
|
||||||
|
|
||||||
this.calculateInterval();
|
|
||||||
|
|
||||||
return this.datasource;
|
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) {
|
issueQueries(datasource: DataSourceApi) {
|
||||||
this.datasource = datasource;
|
this.datasource = datasource;
|
||||||
|
|
||||||
@@ -206,8 +184,9 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
panelId: panel.id,
|
panelId: panel.id,
|
||||||
dashboardId: this.dashboard.id,
|
dashboardId: this.dashboard.id,
|
||||||
timezone: this.dashboard.timezone,
|
timezone: this.dashboard.timezone,
|
||||||
|
timeInfo: this.timeInfo,
|
||||||
timeRange: this.range,
|
timeRange: this.range,
|
||||||
widthPixels: this.resolution, // The pixel width
|
widthPixels: this.width,
|
||||||
maxDataPoints: panel.maxDataPoints,
|
maxDataPoints: panel.maxDataPoints,
|
||||||
minInterval: panel.interval,
|
minInterval: panel.interval,
|
||||||
scopedVars: panel.scopedVars,
|
scopedVars: panel.scopedVars,
|
||||||
@@ -248,25 +227,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
this.processDataError(err);
|
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 };
|
export { MetricsPanelCtrl };
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { escapeHtml, sanitize } from 'app/core/utils/text';
|
|
||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { Emitter, profiler } from 'app/core/core';
|
import { profiler } from 'app/core/core';
|
||||||
import getFactors from 'app/core/utils/factors';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
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 { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { TemplateSrv } from '../templating/template_srv';
|
import { AppEvent, PanelEvents, PanelPluginMeta, AngularPanelMenuItem } from '@grafana/data';
|
||||||
import { getPanelLinksSupplier } from './panellinks/linkSuppliers';
|
|
||||||
import { AppEvent, PanelEvents, PanelPluginMeta, renderMarkdown } from '@grafana/data';
|
|
||||||
import { getLocationSrv } from '@grafana/runtime';
|
|
||||||
import { DashboardModel } from '../dashboard/state';
|
import { DashboardModel } from '../dashboard/state';
|
||||||
|
|
||||||
export class PanelCtrl {
|
export class PanelCtrl {
|
||||||
@@ -32,12 +18,12 @@ export class PanelCtrl {
|
|||||||
$location: any;
|
$location: any;
|
||||||
$timeout: any;
|
$timeout: any;
|
||||||
editModeInitiated: boolean;
|
editModeInitiated: boolean;
|
||||||
height: any;
|
height: number;
|
||||||
|
width: number;
|
||||||
containerHeight: any;
|
containerHeight: any;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
timing: any;
|
timing: any;
|
||||||
maxPanelsPerRowOptions: number[];
|
|
||||||
|
|
||||||
constructor($scope: any, $injector: auto.IInjectorService) {
|
constructor($scope: any, $injector: auto.IInjectorService) {
|
||||||
this.$injector = $injector;
|
this.$injector = $injector;
|
||||||
@@ -74,31 +60,10 @@ export class PanelCtrl {
|
|||||||
this.$scope.$root.appEvent(event, payload);
|
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() {
|
initEditMode() {
|
||||||
if (!this.editModeInitiated) {
|
if (!this.editModeInitiated) {
|
||||||
this.editModeInitiated = true;
|
this.editModeInitiated = true;
|
||||||
this.events.emit(PanelEvents.editModeInitialized);
|
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() {
|
getExtendedMenu() {
|
||||||
const menu = [];
|
const menu: AngularPanelMenuItem[] = [];
|
||||||
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();',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.events.emit(PanelEvents.initPanelActions, menu);
|
this.events.emit(PanelEvents.initPanelActions, menu);
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
@@ -213,93 +98,10 @@ export class PanelCtrl {
|
|||||||
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
|
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatePanelHeight(containerHeight: number) {
|
|
||||||
this.containerHeight = containerHeight;
|
|
||||||
this.height = calculateInnerPanelHeight(this.panel, containerHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(payload?: any) {
|
render(payload?: any) {
|
||||||
this.events.emit(PanelEvents.render, payload);
|
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
|
// overriden from react
|
||||||
onPluginTypeChange = (plugin: PanelPluginMeta) => {};
|
onPluginTypeChange = (plugin: PanelPluginMeta) => {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,14 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import $ from 'jquery';
|
|
||||||
// @ts-ignore
|
|
||||||
import Drop from 'tether-drop';
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import baron from 'baron';
|
import baron from 'baron';
|
||||||
import { PanelEvents } from '@grafana/data';
|
import { PanelEvents } from '@grafana/data';
|
||||||
import { getLocationSrv } from '@grafana/runtime';
|
import { PanelModel } from '../dashboard/state';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { PanelCtrl } from './panel_ctrl';
|
||||||
|
|
||||||
const module = angular.module('grafana.directives');
|
const module = angular.module('grafana.directives');
|
||||||
|
|
||||||
const panelTemplate = `
|
const panelTemplate = `
|
||||||
<div class="panel-container" ng-class="{'panel-container--no-title': !ctrl.panel.title.length}">
|
<ng-transclude class="panel-height-helper"></ng-transclude>
|
||||||
<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>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||||
@@ -39,52 +18,19 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
transclude: true,
|
transclude: true,
|
||||||
scope: { ctrl: '=' },
|
scope: { ctrl: '=' },
|
||||||
link: (scope: any, elem) => {
|
link: (scope: any, elem) => {
|
||||||
const panelContainer = elem.find('.panel-container');
|
const ctrl: PanelCtrl = scope.ctrl;
|
||||||
const panelContent = elem.find('.panel-content');
|
const panel: PanelModel = scope.ctrl.panel;
|
||||||
const cornerInfoElem = elem.find('.panel-info-corner');
|
|
||||||
const ctrl = scope.ctrl;
|
|
||||||
ctrl.selectors = e2e.pages.Dashboard.Panels.Panel.selectors;
|
|
||||||
let infoDrop: any;
|
|
||||||
let panelScrollbar: any;
|
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() {
|
function resizeScrollableContent() {
|
||||||
if (panelScrollbar) {
|
if (panelScrollbar) {
|
||||||
panelScrollbar.update();
|
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, () => {
|
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 scrollRootClass = 'baron baron__root baron__clipper panel-content--scrollable';
|
||||||
const scrollerClass = 'baron__scroller';
|
const scrollerClass = 'baron__scroller';
|
||||||
const scrollBarHTML = `
|
const scrollBarHTML = `
|
||||||
@@ -93,8 +39,8 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const scrollRoot = panelContent;
|
const scrollRoot = elem;
|
||||||
const scroller = panelContent.find(':first').find(':first');
|
const scroller = elem.find(':first').find(':first');
|
||||||
|
|
||||||
scrollRoot.addClass(scrollRootClass);
|
scrollRoot.addClass(scrollRootClass);
|
||||||
$(scrollBarHTML).appendTo(scrollRoot);
|
$(scrollBarHTML).appendTo(scrollRoot);
|
||||||
@@ -112,112 +58,44 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ctrl.events.on(PanelEvents.panelSizeChanged, () => {
|
function onPanelSizeChanged() {
|
||||||
ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
|
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
resizeScrollableContent();
|
resizeScrollableContent();
|
||||||
ctrl.render();
|
ctrl.render();
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
ctrl.events.on(PanelEvents.viewModeChanged, () => {
|
function onViewModeChanged() {
|
||||||
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
|
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// then recalc style
|
|
||||||
ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
|
|
||||||
// then wait another cycle (this might not be needed)
|
// then wait another cycle (this might not be needed)
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
ctrl.render();
|
ctrl.render();
|
||||||
resizeScrollableContent();
|
resizeScrollableContent();
|
||||||
});
|
});
|
||||||
}, 10);
|
}, 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);
|
function onPanelModelRender(payload?: any) {
|
||||||
scope.$watchCollection('ctrl.panel.links', updatePanelCornerInfo);
|
ctrl.height = scope.$parent.$parent.size.height;
|
||||||
|
ctrl.width = scope.$parent.$parent.size.width;
|
||||||
|
}
|
||||||
|
|
||||||
elem.on('mouseenter', mouseEnter);
|
function onPanelModelRefresh() {
|
||||||
elem.on('mouseleave', mouseLeave);
|
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', () => {
|
scope.$on('$destroy', () => {
|
||||||
elem.off();
|
elem.off();
|
||||||
cornerInfoElem.off();
|
|
||||||
|
|
||||||
if (infoDrop) {
|
panel.events.emit(PanelEvents.panelTeardown);
|
||||||
infoDrop.destroy();
|
panel.events.removeAllListeners();
|
||||||
}
|
|
||||||
|
|
||||||
if (panelScrollbar) {
|
if (panelScrollbar) {
|
||||||
panelScrollbar.dispose();
|
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';
|
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
|
||||||
|
|
||||||
describe('MetricsPanelCtrl', () => {
|
describe('MetricsPanelCtrl', () => {
|
||||||
describe('when getting additional menu items', () => {
|
describe('can setup', () => {
|
||||||
describe('and has no datasource set but user has access to explore', () => {
|
it('should return controller', async () => {
|
||||||
it('should not return any items', async () => {
|
const ctrl = setupController({ hasAccessToExplore: true });
|
||||||
const ctrl = setupController({ hasAccessToExplore: true });
|
expect((await ctrl.getAdditionalMenuItems()).length).toBe(0);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ export default class InfluxQueryModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addGroupBy(value: string) {
|
addGroupBy(value: string) {
|
||||||
const stringParts = value.match(/^(\w+)\((.*)\)$/);
|
let stringParts = value.match(/^(\w+)\((.*)\)$/);
|
||||||
|
|
||||||
|
if (!stringParts || !this.target.groupBy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const typePart = stringParts[1];
|
const typePart = stringParts[1];
|
||||||
const arg = stringParts[2];
|
const arg = stringParts[2];
|
||||||
const partModel = queryPart.create({ type: typePart, params: [arg] });
|
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);
|
return (yLeft.min >= 0 && yRight.max <= 0) || (yLeft.max <= 0 && yRight.min >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRate(yLeft: AxisSide, yRight: AxisSide) {
|
function getRate(yLeft: AxisSide, yRight: AxisSide): number {
|
||||||
let rateLeft, rateRight, rate;
|
|
||||||
if (checkTwoCross(yLeft, yRight)) {
|
if (checkTwoCross(yLeft, yRight)) {
|
||||||
rateLeft = yRight.min ? yLeft.min / yRight.min : 0;
|
const rateLeft = yRight.min ? yLeft.min / yRight.min : 0;
|
||||||
rateRight = yRight.max ? yLeft.max / yRight.max : 0;
|
const 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]);
|
|
||||||
|
|
||||||
rateLeft = downLeft ? upLeft / downLeft : upLeft;
|
return rateLeft > rateRight ? rateLeft : rateRight;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.loading = false;
|
||||||
this.alertState = result.alertState;
|
this.alertState = result.alertState;
|
||||||
this.annotations = result.annotations;
|
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);
|
this.render(this.seriesList);
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('grafanaGraph', () => {
|
|||||||
panel: {
|
panel: {
|
||||||
events: {
|
events: {
|
||||||
on: () => {},
|
on: () => {},
|
||||||
|
emit: () => {},
|
||||||
},
|
},
|
||||||
legend: {},
|
legend: {},
|
||||||
grid: {},
|
grid: {},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe('GraphCtrl', () => {
|
|||||||
GraphCtrl.prototype.panel = {
|
GraphCtrl.prototype.panel = {
|
||||||
events: {
|
events: {
|
||||||
on: () => {},
|
on: () => {},
|
||||||
|
emit: () => {},
|
||||||
},
|
},
|
||||||
gridPos: {
|
gridPos: {
|
||||||
w: 100,
|
w: 100,
|
||||||
|
|||||||
@@ -577,8 +577,8 @@ export class HeatmapRenderer {
|
|||||||
|
|
||||||
highlightCard(event: any) {
|
highlightCard(event: any) {
|
||||||
const color = d3.select(event.target).style('fill');
|
const color = d3.select(event.target).style('fill');
|
||||||
const highlightColor = d3.color(color).darker(2);
|
const highlightColor = d3.color(color)!.darker(2);
|
||||||
const strokeColor = d3.color(color).brighter(4);
|
const strokeColor = d3.color(color)!.brighter(4);
|
||||||
const currentCard = d3.select(event.target);
|
const currentCard = d3.select(event.target);
|
||||||
this.tooltip.originalFillColor = color;
|
this.tooltip.originalFillColor = color;
|
||||||
currentCard
|
currentCard
|
||||||
|
|||||||
@@ -348,9 +348,12 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
const panel = ctrl.panel;
|
const panel = ctrl.panel;
|
||||||
const templateSrv = this.templateSrv;
|
const templateSrv = this.templateSrv;
|
||||||
let linkInfo: LinkModel<any> | null = null;
|
let linkInfo: LinkModel<any> | null = null;
|
||||||
const $panelContainer = elem.find('.panel-container');
|
|
||||||
elem = elem.find('.singlestat-panel');
|
elem = elem.find('.singlestat-panel');
|
||||||
|
|
||||||
|
function getPanelContainer() {
|
||||||
|
return elem.closest('.panel-container');
|
||||||
|
}
|
||||||
|
|
||||||
function applyColoringThresholds(valueString: string) {
|
function applyColoringThresholds(valueString: string) {
|
||||||
const data = ctrl.data;
|
const data = ctrl.data;
|
||||||
const color = getColorForValue(data, data.value);
|
const color = getColorForValue(data, data.value);
|
||||||
@@ -588,18 +591,18 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
if (panel.colorBackground) {
|
if (panel.colorBackground) {
|
||||||
const color = getColorForValue(data, data.display.numeric);
|
const color = getColorForValue(data, data.display.numeric);
|
||||||
if (color) {
|
if (color) {
|
||||||
$panelContainer.css('background-color', color);
|
getPanelContainer().css('background-color', color);
|
||||||
if (scope.fullscreen) {
|
if (scope.fullscreen) {
|
||||||
elem.css('background-color', color);
|
elem.css('background-color', color);
|
||||||
} else {
|
} else {
|
||||||
elem.css('background-color', '');
|
elem.css('background-color', '');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$panelContainer.css('background-color', '');
|
getPanelContainer().css('background-color', '');
|
||||||
elem.css('background-color', '');
|
elem.css('background-color', '');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$panelContainer.css('background-color', '');
|
getPanelContainer().css('background-color', '');
|
||||||
elem.css('background-color', '');
|
elem.css('background-color', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,23 +26,21 @@ describe('SingleStatCtrl', () => {
|
|||||||
|
|
||||||
const $sanitize = {};
|
const $sanitize = {};
|
||||||
|
|
||||||
SingleStatCtrl.prototype.panel = {
|
|
||||||
events: {
|
|
||||||
on: () => {},
|
|
||||||
emit: () => {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
SingleStatCtrl.prototype.dashboard = ({
|
SingleStatCtrl.prototype.dashboard = ({
|
||||||
getTimezone: jest.fn(() => 'utc'),
|
getTimezone: jest.fn(() => 'utc'),
|
||||||
} as any) as DashboardModel;
|
} as any) as DashboardModel;
|
||||||
SingleStatCtrl.prototype.events = {
|
|
||||||
on: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
function singleStatScenario(desc: string, func: any) {
|
function singleStatScenario(desc: string, func: any) {
|
||||||
describe(desc, () => {
|
describe(desc, () => {
|
||||||
ctx.setup = (setupFunc: any) => {
|
ctx.setup = (setupFunc: any) => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
SingleStatCtrl.prototype.panel = {
|
||||||
|
events: {
|
||||||
|
on: () => {},
|
||||||
|
emit: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize);
|
ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize);
|
||||||
setupFunc();
|
setupFunc();
|
||||||
@@ -335,9 +333,6 @@ describe('SingleStatCtrl', () => {
|
|||||||
singleStatScenario('with default values', (ctx: TestContext) => {
|
singleStatScenario('with default values', (ctx: TestContext) => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.input = tableData;
|
ctx.input = tableData;
|
||||||
ctx.ctrl.panel = {
|
|
||||||
emit: () => {},
|
|
||||||
};
|
|
||||||
ctx.ctrl.panel.tableColumn = 'mean';
|
ctx.ctrl.panel.tableColumn = 'mean';
|
||||||
ctx.ctrl.panel.format = 'none';
|
ctx.ctrl.panel.format = 'none';
|
||||||
});
|
});
|
||||||
@@ -386,7 +381,7 @@ describe('SingleStatCtrl', () => {
|
|||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.input = tableData;
|
ctx.input = tableData;
|
||||||
ctx.input[0].rows[0] = [1492759673649, 'ignore1', 10, 'ignore2'];
|
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.tableColumn = 'mean';
|
||||||
ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
|
ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import { transformDataToTable } from './transformers';
|
|||||||
import { tablePanelEditor } from './editor';
|
import { tablePanelEditor } from './editor';
|
||||||
import { columnOptionsTab } from './column_options';
|
import { columnOptionsTab } from './column_options';
|
||||||
import { TableRenderer } from './renderer';
|
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 { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { PanelEvents } from '@grafana/data';
|
|
||||||
import { CoreEvents } from 'app/types';
|
import { CoreEvents } from 'app/types';
|
||||||
|
|
||||||
class TablePanelCtrl extends MetricsPanelCtrl {
|
export class TablePanelCtrl extends MetricsPanelCtrl {
|
||||||
static templateUrl = 'module.html';
|
static templateUrl = 'module.html';
|
||||||
|
|
||||||
pageIndex: number;
|
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 {
|
.panel-menu-container {
|
||||||
width: 1px;
|
width: 0px;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0.45em 0 0.45em 1.1em;
|
padding: 0.45em 1.1em;
|
||||||
border-bottom: 2px solid $body-bg;
|
border-bottom: 2px solid $body-bg;
|
||||||
border-right: 2px solid $body-bg;
|
border-right: 2px solid $body-bg;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,15 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-alert-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-has-alert {
|
.panel-has-alert {
|
||||||
|
.panel-alert-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-alert-icon:before {
|
.panel-alert-icon:before {
|
||||||
content: '\e611';
|
content: '\e611';
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -40,7 +48,6 @@
|
|||||||
.panel-alert-state {
|
.panel-alert-state {
|
||||||
&--alerting {
|
&--alerting {
|
||||||
box-shadow: 0 0 10px rgba($critical, 0.5);
|
box-shadow: 0 0 10px rgba($critical, 0.5);
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.panel-alert-icon:before {
|
.panel-alert-icon:before {
|
||||||
color: $critical;
|
color: $critical;
|
||||||
|
|||||||
Reference in New Issue
Block a user