Browse Source

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

12759 panel header menu in React (standard options only)
Torkel Ödegaard 7 years ago
parent
commit
3b4d8c9b9b

+ 4 - 4
public/app/core/controllers/json_editor_ctrl.ts

@@ -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;

+ 3 - 1
public/app/core/reducers/location.ts

@@ -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,
       };
     }

+ 1 - 1
public/app/core/services/bridge_srv.ts

@@ -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;
 

+ 8 - 34
public/app/features/dashboard/dashboard_ctrl.ts

@@ -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() {

+ 0 - 5
public/app/features/dashboard/dashboard_model.ts

@@ -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 = {};

+ 6 - 2
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -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>
       );
     }

+ 6 - 6
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -1,4 +1,4 @@
-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 (

+ 10 - 8
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -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} />

+ 0 - 83
public/app/features/dashboard/dashgrid/PanelHeader.tsx

@@ -1,83 +0,0 @@
-import React 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 {
-  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,
-        },
-      })
-    );
-  };
-
-  render() {
-    const isFullscreen = false;
-    const isLoading = false;
-    const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
-
-    return (
-      <div className={panelHeaderClass}>
-        <span className="panel-info-corner">
-          <i className="fa" />
-          <span className="panel-info-corner-inner" />
-        </span>
-
-        {isLoading && (
-          <span className="panel-loading">
-            <i className="fa fa-spinner fa-spin" />
-          </span>
-        )}
-
-        <div className="panel-title-container">
-          <span 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>
-            <span className="panel-time-info">
-              <i className="fa fa-clock-o" /> 4m
-            </span>
-          </span>
-        </div>
-      </div>
-    );
-  }
-}

+ 51 - 0
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -0,0 +1,51 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+
+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 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}>
+        <span className="panel-info-corner">
+          <i className="fa" />
+          <span className="panel-info-corner-inner" />
+        </span>
+
+        {isLoading && (
+          <span className="panel-loading">
+            <i className="fa fa-spinner fa-spin" />
+          </span>
+        )}
+
+        <div className="panel-title-container">
+          <div className="panel-title">
+            <span className="icon-gf panel-alert-icon" />
+            <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>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}

+ 40 - 0
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx

@@ -0,0 +1,40 @@
+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>;
+  }
+}

+ 23 - 0
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx

@@ -0,0 +1,23 @@
+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>
+  );
+};

+ 6 - 5
public/app/features/dashboard/export/export_modal.ts

@@ -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();

+ 2 - 0
public/app/features/dashboard/shareModalCtrl.ts

@@ -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' }];

+ 120 - 0
public/app/features/dashboard/utils/getPanelMenu.ts

@@ -0,0 +1,120 @@
+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;
+};

+ 86 - 0
public/app/features/dashboard/utils/panel.ts

@@ -0,0 +1,86 @@
+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);
+};

+ 12 - 41
public/app/features/panel/panel_ctrl.ts

@@ -1,11 +1,15 @@
 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() {

+ 1 - 3
public/app/features/panel/viz_tab.ts

@@ -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 = `

+ 9 - 0
public/app/types/panel.ts

@@ -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[];
+}

+ 5 - 0
public/sass/components/_dropdown.scss

@@ -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;

+ 0 - 1
public/sass/pages/_dashboard.scss

@@ -138,7 +138,6 @@ div.flot-text {
   padding: 3px 5px;
   visibility: hidden;
   opacity: 0;
-  position: absolute;
   width: 16px;
   height: 16px;
   left: 1px;