Commit 3b4d8c9b by Torkel Ödegaard Committed by GitHub

Merge pull request #13984 from grafana/12759-panel-header-standard-menu-only

12759 panel header menu in React (standard options only)
parents dcb50150 d7655e0b
......@@ -4,13 +4,13 @@ import coreModule from '../core_module';
export class JsonEditorCtrl {
/** @ngInject */
constructor($scope) {
$scope.json = angular.toJson($scope.object, true);
$scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
$scope.canCopy = $scope.enableCopy;
$scope.json = angular.toJson($scope.model.object, true);
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.contextSrv.isEditor;
$scope.canCopy = $scope.model.enableCopy;
$scope.update = () => {
const newObject = angular.fromJson($scope.json);
$scope.updateHandler(newObject, $scope.object);
$scope.model.updateHandler(newObject, $scope.model.object);
};
$scope.getContentForClipboard = () => $scope.json;
......
......@@ -23,7 +23,9 @@ export const locationReducer = (state = initialState, action: Action): LocationS
return {
url: renderUrl(path || state.path, query),
path: path || state.path,
query: query,
query: {
...query,
},
routeParams: routeParams || state.routeParams,
};
}
......
......@@ -4,7 +4,7 @@ import { store } from 'app/store/configureStore';
import locationUtil from 'app/core/utils/location_util';
import { updateLocation } from 'app/core/actions';
// Services that handles angular -> mobx store sync & other react <-> angular sync
// Services that handles angular -> redux store sync & other react <-> angular sync
export class BridgeSrv {
private fullPageReloadRoutes;
......
......@@ -2,13 +2,13 @@
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import coreModule from 'app/core/core_module';
import { removePanel } from 'app/features/dashboard/utils/panel';
// Services
import { AnnotationsSrv } from '../annotations/annotations_srv';
// Types
import { DashboardModel } from './dashboard_model';
import { PanelModel } from './panel_model';
export class DashboardCtrl {
dashboard: DashboardModel;
......@@ -19,7 +19,6 @@ export class DashboardCtrl {
/** @ngInject */
constructor(
private $scope,
private $rootScope,
private keybindingSrv,
private timeSrv,
private variableSrv,
......@@ -112,12 +111,14 @@ export class DashboardCtrl {
}
showJsonEditor(evt, options) {
const editScope = this.$rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
const model = {
object: options.object,
updateHandler: options.updateHandler,
};
this.$scope.appEvent('show-dash-editor', {
src: 'public/app/partials/edit_json.html',
scope: editScope,
model: model,
});
}
......@@ -136,34 +137,7 @@ export class DashboardCtrl {
}
const panelInfo = this.dashboard.getPanelInfoById(options.panelId);
this.removePanel(panelInfo.panel, true);
}
removePanel(panel: PanelModel, ask: boolean) {
// confirm deletion
if (ask !== false) {
let text2, confirmText;
if (panel.alert) {
text2 = 'Panel includes an alert rule, removing panel will also remove alert rule';
confirmText = 'YES';
}
this.$scope.appEvent('confirm-modal', {
title: 'Remove Panel',
text: 'Are you sure you want to remove this panel?',
text2: text2,
icon: 'fa-trash',
confirmText: confirmText,
yesText: 'Remove',
onConfirm: () => {
this.removePanel(panel, false);
},
});
return;
}
this.dashboard.removePanel(panel);
removePanel(this.dashboard, panelInfo.panel, true);
}
onDestroy() {
......
......@@ -232,11 +232,6 @@ export class DashboardModel {
return this.meta.fullscreen && !panel.fullscreen;
}
changePanelType(panel: PanelModel, pluginId: string) {
panel.changeType(pluginId);
this.events.emit('panel-type-changed', panel);
}
private ensureListExist(data) {
if (!data) {
data = {};
......
......@@ -83,7 +83,6 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
}
buildLayout() {
......@@ -176,7 +175,12 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
panelElements.push(
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
<DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
<DashboardPanel
panel={panel}
dashboard={this.props.dashboard}
isEditing={panel.isEditing}
isFullscreen={panel.fullscreen}
/>
</div>
);
}
......
import React from 'react';
import React, { PureComponent } from 'react';
import config from 'app/core/config';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
......@@ -11,16 +11,17 @@ import { PanelChrome } from './PanelChrome';
import { PanelEditor } from './PanelEditor';
export interface Props {
panelType: string;
panel: PanelModel;
dashboard: DashboardModel;
isEditing: boolean;
isFullscreen: boolean;
}
export interface State {
pluginExports: PluginExports;
}
export class DashboardPanel extends React.Component<Props, State> {
export class DashboardPanel extends PureComponent<Props, State> {
element: any;
angularPanel: AngularComponent;
pluginInfo: any;
......@@ -113,9 +114,8 @@ export class DashboardPanel extends React.Component<Props, State> {
renderReactPanel() {
const { pluginExports } = this.state;
const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper';
const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
const containerClass = this.props.isEditing ? 'panel-editor-container' : 'panel-height-helper';
const panelWrapperClass = this.props.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
// this might look strange with these classes that change when edit, but
// I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel
return (
......
......@@ -5,7 +5,7 @@ import React, { ComponentClass, PureComponent } from 'react';
import { getTimeSrv } from '../time_srv';
// Components
import { PanelHeader } from './PanelHeader';
import { PanelHeader } from './PanelHeader/PanelHeader';
import { DataPanel } from './DataPanel';
// Types
......@@ -49,17 +49,19 @@ export class PanelChrome extends PureComponent<Props, State> {
const timeSrv = getTimeSrv();
const timeRange = timeSrv.timeRange();
this.setState({
this.setState(prevState => ({
...prevState,
refreshCounter: this.state.refreshCounter + 1,
timeRange: timeRange,
});
}));
};
onRender = () => {
console.log('onRender');
this.setState({
this.setState(prevState => ({
...prevState,
renderCounter: this.state.renderCounter + 1,
});
}));
};
get isVisible() {
......@@ -68,12 +70,12 @@ export class PanelChrome extends PureComponent<Props, State> {
render() {
const { panel, dashboard } = this.props;
const { refreshCounter, timeRange, renderCounter } = this.state;
const { datasource, targets } = panel;
const { timeRange, renderCounter, refreshCounter } = this.state;
const PanelComponent = this.props.component;
console.log('Panel chrome render');
console.log('panelChrome render');
return (
<div className="panel-container">
<PanelHeader panel={panel} dashboard={dashboard} />
......
import React from 'react';
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { store } from 'app/store/configureStore';
import { updateLocation } from 'app/core/actions';
interface PanelHeaderProps {
import { PanelHeaderMenu } from './PanelHeaderMenu';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
}
export class PanelHeader extends React.Component<PanelHeaderProps, any> {
onEditPanel = () => {
store.dispatch(
updateLocation({
query: {
panelId: this.props.panel.id,
edit: true,
fullscreen: true,
},
})
);
};
onViewPanel = () => {
store.dispatch(
updateLocation({
query: {
panelId: this.props.panel.id,
edit: false,
fullscreen: true,
},
})
);
};
export class PanelHeader extends PureComponent<Props> {
render() {
const isFullscreen = false;
const isLoading = false;
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
const { panel, dashboard } = this.props;
return (
<div className={panelHeaderClass}>
......@@ -54,28 +32,18 @@ export class PanelHeader extends React.Component<PanelHeaderProps, any> {
)}
<div className="panel-title-container">
<span className="panel-title">
<div className="panel-title">
<span className="icon-gf panel-alert-icon" />
<span className="panel-title-text">{this.props.panel.title}</span>
<span className="panel-menu-container dropdown">
<span className="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown" />
<ul className="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
<li>
<a onClick={this.onEditPanel}>
<i className="fa fa-fw fa-edit" /> Edit
</a>
</li>
<li>
<a onClick={this.onViewPanel}>
<i className="fa fa-fw fa-eye" /> View
</a>
</li>
</ul>
<span className="panel-title-text" data-toggle="dropdown">
{panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
</span>
<PanelHeaderMenu panel={panel} dashboard={dashboard} />
<span className="panel-time-info">
<i className="fa fa-clock-o" /> 4m
</span>
</span>
</div>
</div>
</div>
);
......
import React, { PureComponent } from 'react';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
import { PanelMenuItem } from 'app/types/panel';
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
}
export class PanelHeaderMenu extends PureComponent<Props> {
renderItems = (menu: PanelMenuItem[], isSubMenu = false) => {
return (
<ul className="dropdown-menu dropdown-menu--menu panel-menu" role={isSubMenu ? '' : 'menu'}>
{menu.map((menuItem, idx: number) => {
return (
<PanelHeaderMenuItem
key={`${menuItem.text}${idx}`}
type={menuItem.type}
text={menuItem.text}
iconClassName={menuItem.iconClassName}
onClick={menuItem.onClick}
shortcut={menuItem.shortcut}
>
{menuItem.subMenu && this.renderItems(menuItem.subMenu, true)}
</PanelHeaderMenuItem>
);
})}
</ul>
);
};
render() {
const { dashboard, panel } = this.props;
const menu = getPanelMenu(dashboard, panel);
return <div className="panel-menu-container dropdown">{this.renderItems(menu)}</div>;
}
}
import React, { SFC } from 'react';
import { PanelMenuItem } from 'app/types/panel';
interface Props {
children: any;
}
export const PanelHeaderMenuItem: SFC<Props & PanelMenuItem> = props => {
const isSubMenu = props.type === 'submenu';
const isDivider = props.type === 'divider';
return isDivider ? (
<li className="divider" />
) : (
<li className={isSubMenu ? 'dropdown-submenu' : null}>
<a onClick={props.onClick}>
{props.iconClassName && <i className={props.iconClassName} />}
<span className="dropdown-item-text">{props.text}</span>
{props.shortcut && <span className="dropdown-menu-item-shortcut">{props.shortcut}</span>}
</a>
{props.children}
</li>
);
};
......@@ -48,14 +48,15 @@ export class DashExportCtrl {
saveAs(blob, dash.title + '-' + new Date().getTime() + '.json');
}
private openJsonModal(clone: any) {
const editScope = this.$rootScope.$new();
editScope.object = clone;
editScope.enableCopy = true;
private openJsonModal(clone: object) {
const model = {
object: clone,
enableCopy: true,
};
this.$rootScope.appEvent('show-modal', {
src: 'public/app/partials/edit_json.html',
scope: editScope,
model: model,
});
this.dismiss();
......
......@@ -12,6 +12,8 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv,
$scope.editor = { index: $scope.tabIndex || 0 };
$scope.init = () => {
$scope.panel = $scope.model && $scope.model.panel ? $scope.model.panel : $scope.panel; // React pass panel and dashboard in the "model" property
$scope.dashboard = $scope.model && $scope.model.dashboard ? $scope.model.dashboard : $scope.dashboard; // ^
$scope.modeSharePanel = $scope.panel ? true : false;
$scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
......
import { updateLocation } from 'app/core/actions';
import { store } from 'app/store/configureStore';
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelMenuItem } from 'app/types/panel';
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
const onViewPanel = () => {
store.dispatch(
updateLocation({
query: {
panelId: panel.id,
edit: false,
fullscreen: true,
},
partial: true,
})
);
};
const onEditPanel = () => {
store.dispatch(
updateLocation({
query: {
panelId: panel.id,
edit: true,
fullscreen: true,
},
partial: true,
})
);
};
const onSharePanel = () => {
sharePanel(dashboard, panel);
};
const onDuplicatePanel = () => {
duplicatePanel(dashboard, panel);
};
const onCopyPanel = () => {
copyPanel(panel);
};
const onEditPanelJson = () => {
editPanelJson(dashboard, panel);
};
const onRemovePanel = () => {
removePanel(dashboard, panel, true);
};
const menu: PanelMenuItem[] = [];
menu.push({
text: 'View',
iconClassName: 'fa fa-fw fa-eye',
onClick: onViewPanel,
shortcut: 'v',
});
if (dashboard.meta.canEdit) {
menu.push({
text: 'Edit',
iconClassName: 'fa fa-fw fa-edit',
onClick: onEditPanel,
shortcut: 'e',
});
}
menu.push({
text: 'Share',
iconClassName: 'fa fa-fw fa-share',
onClick: onSharePanel,
shortcut: 'p s',
});
const subMenu: PanelMenuItem[] = [];
if (!panel.fullscreen && dashboard.meta.canEdit) {
subMenu.push({
text: 'Duplicate',
onClick: onDuplicatePanel,
shortcut: 'p d',
});
subMenu.push({
text: 'Copy',
onClick: onCopyPanel,
});
}
subMenu.push({
text: 'Panel JSON',
onClick: onEditPanelJson,
});
menu.push({
type: 'submenu',
text: 'More...',
iconClassName: 'fa fa-fw fa-cube',
subMenu: subMenu,
});
if (dashboard.meta.canEdit) {
menu.push({ type: 'divider' });
menu.push({
text: 'Remove',
iconClassName: 'fa fa-fw fa-trash',
onClick: onRemovePanel,
shortcut: 'p r',
});
}
return menu;
};
import appEvents from 'app/core/app_events';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import store from 'app/core/store';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: boolean) => {
// confirm deletion
if (ask !== false) {
const text2 = panel.alert ? 'Panel includes an alert rule, removing panel will also remove alert rule' : null;
const confirmText = panel.alert ? 'YES' : null;
appEvents.emit('confirm-modal', {
title: 'Remove Panel',
text: 'Are you sure you want to remove this panel?',
text2: text2,
icon: 'fa-trash',
confirmText: confirmText,
yesText: 'Remove',
onConfirm: () => removePanel(dashboard, panel, false),
});
return;
}
dashboard.removePanel(panel);
};
export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) => {
dashboard.duplicatePanel(panel);
};
export const copyPanel = (panel: PanelModel) => {
store.set(LS_PANEL_COPY_KEY, JSON.stringify(panel.getSaveModel()));
appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']);
};
const replacePanel = (dashboard: DashboardModel, newPanel: PanelModel, oldPanel: PanelModel) => {
const index = dashboard.panels.findIndex(panel => {
return panel.id === oldPanel.id;
});
const deletedPanel = dashboard.panels.splice(index, 1);
dashboard.events.emit('panel-removed', deletedPanel);
newPanel = new PanelModel(newPanel);
newPanel.id = oldPanel.id;
dashboard.panels.splice(index, 0, newPanel);
dashboard.sortPanelsByGridPos();
dashboard.events.emit('panel-added', newPanel);
};
export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
const model = {
object: panel.getSaveModel(),
updateHandler: (newPanel: PanelModel, oldPanel: PanelModel) => {
replacePanel(dashboard, newPanel, oldPanel);
},
enableCopy: true,
};
appEvents.emit('show-modal', {
src: 'public/app/partials/edit_json.html',
model: model,
});
};
export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => {
appEvents.emit('show-modal', {
src: 'public/app/features/dashboard/partials/shareModal.html',
model: {
dashboard: dashboard,
panel: panel,
},
});
};
export const refreshPanel = (panel: PanelModel) => {
panel.refresh();
};
export const toggleLegend = (panel: PanelModel) => {
console.log('Toggle legend is not implemented yet');
// We need to set panel.legend defaults first
// panel.legend.show = !panel.legend.show;
refreshPanel(panel);
};
import config from 'app/core/config';
import _ from 'lodash';
import $ from 'jquery';
import { appEvents, profiler } from 'app/core/core';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { profiler } from 'app/core/core';
import {
duplicatePanel,
copyPanel as copyPanelUtil,
editPanelJson as editPanelJsonUtil,
sharePanel as sharePanelUtil,
} from 'app/features/dashboard/utils/panel';
import Remarkable from 'remarkable';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants';
import store from 'app/core/store';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
const TITLE_HEIGHT = 27;
const PANEL_BORDER = 2;
......@@ -241,7 +245,7 @@ export class PanelCtrl {
}
duplicate() {
this.dashboard.duplicatePanel(this.panel);
duplicatePanel(this.dashboard, this.panel);
}
removePanel() {
......@@ -251,48 +255,15 @@ export class PanelCtrl {
}
editPanelJson() {
const editScope = this.$scope.$root.$new();
editScope.object = this.panel.getSaveModel();
editScope.updateHandler = this.replacePanel.bind(this);
editScope.enableCopy = true;
this.publishAppEvent('show-modal', {
src: 'public/app/partials/edit_json.html',
scope: editScope,
});
editPanelJsonUtil(this.dashboard, this.panel);
}
copyPanel() {
store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel()));
appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']);
}
replacePanel(newPanel, oldPanel) {
const dashboard = this.dashboard;
const index = _.findIndex(dashboard.panels, panel => {
return panel.id === oldPanel.id;
});
const deletedPanel = dashboard.panels.splice(index, 1);
this.dashboard.events.emit('panel-removed', deletedPanel);
newPanel = new PanelModel(newPanel);
newPanel.id = oldPanel.id;
dashboard.panels.splice(index, 0, newPanel);
dashboard.sortPanelsByGridPos();
dashboard.events.emit('panel-added', newPanel);
copyPanelUtil(this.panel);
}
sharePanel() {
const shareScope = this.$scope.$new();
shareScope.panel = this.panel;
shareScope.dashboard = this.dashboard;
this.publishAppEvent('show-modal', {
src: 'public/app/features/dashboard/partials/shareModal.html',
scope: shareScope,
});
sharePanelUtil(this.dashboard, this.panel);
}
getInfoMode() {
......
......@@ -16,9 +16,7 @@ export class VizTabCtrl {
$scope.ctrl = this;
}
onTypeChanged = (plugin: PanelPlugin) => {
this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id);
};
onTypeChanged = (plugin: PanelPlugin) => {};
}
const template = `
......
......@@ -12,3 +12,12 @@ export interface PanelOptionsProps<T = any> {
options: T;
onChange: (options: T) => void;
}
export interface PanelMenuItem {
type?: 'submenu' | 'divider';
text?: string;
iconClassName?: string;
onClick?: () => void;
shortcut?: string;
subMenu?: PanelMenuItem[];
}
......@@ -183,6 +183,11 @@
display: block;
}
& > .dropdown > .dropdown-menu {
// Panel menu. TODO: See if we can merge this with above
display: block;
}
&.cascade-open {
.dropdown-menu {
display: block;
......
......@@ -138,7 +138,6 @@ div.flot-text {
padding: 3px 5px;
visibility: hidden;
opacity: 0;
position: absolute;
width: 16px;
height: 16px;
left: 1px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment