Browse Source

Merge branch 'react-panels' of github.com:grafana/grafana into react-panels

Torkel Ödegaard 7 years ago
parent
commit
e052e165e9

+ 12 - 1
public/app/core/config.ts

@@ -7,9 +7,20 @@ export interface BuildInfo {
   env: string;
 }
 
+export interface PanelPlugin {
+  id: string;
+  name: string;
+  meta: any;
+  hideFromList: boolean;
+  module: string;
+  baseUrl: string;
+  info: any;
+  sort: number;
+}
+
 export class Settings {
   datasources: any;
-  panels: any;
+  panels: PanelPlugin[];
   appSubUrl: string;
   window_title_prefix: string;
   buildInfo: BuildInfo;

+ 1 - 1
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -139,7 +139,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   }
 
   onViewModeChanged(payload) {
-    this.setState({ animated: payload.fullscreen });
+    this.setState({ animated: !payload.fullscreen });
   }
 
   updateGridPos(item, layout) {

+ 15 - 12
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -5,24 +5,27 @@ import { DashboardModel } from '../dashboard_model';
 import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader';
 import { DashboardRow } from './DashboardRow';
 import { AddPanelPanel } from './AddPanelPanel';
-import { importPluginModule } from 'app/features/plugins/plugin_loader';
+import { importPluginModule, PluginExports } from 'app/features/plugins/plugin_loader';
 import { PanelChrome } from './PanelChrome';
 
-export interface DashboardPanelProps {
+export interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
 }
 
-export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
+export interface State {
+  pluginExports: PluginExports;
+}
+
+export class DashboardPanel extends React.Component<Props, State> {
   element: any;
   angularPanel: AngularComponent;
   pluginInfo: any;
-  pluginExports: any;
   specialPanels = {};
 
   constructor(props) {
     super(props);
-    this.state = {};
+    this.state = { pluginExports: null };
 
     this.specialPanels['row'] = this.renderRow.bind(this);
     this.specialPanels['add-panel'] = this.renderAddPanel.bind(this);
@@ -32,8 +35,7 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
 
       // load panel plugin
       importPluginModule(this.pluginInfo.module).then(pluginExports => {
-        this.pluginExports = pluginExports;
-        this.forceUpdate();
+        this.setState({ pluginExports: pluginExports });
       });
     }
   }
@@ -51,8 +53,7 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
   }
 
   componentDidUpdate() {
-    // skip loading angular component if we have no element
-    // or we have already loaded it
+    // skip loading angular component if we have no element or we have already loaded it
     if (!this.element || this.angularPanel) {
       return;
     }
@@ -70,18 +71,20 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
   }
 
   render() {
+    const { pluginExports } = this.state;
+
     if (this.isSpecial()) {
       return this.specialPanels[this.props.panel.type]();
     }
 
-    if (!this.pluginExports) {
+    if (!pluginExports) {
       return null;
     }
 
-    if (this.pluginExports.PanelComponent) {
+    if (pluginExports.PanelComponent) {
       return (
         <PanelChrome
-          component={this.pluginExports.PanelComponent}
+          component={pluginExports.PanelComponent}
           panel={this.props.panel}
           dashboard={this.props.dashboard}
         />

+ 85 - 0
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -0,0 +1,85 @@
+import React, { Component, ComponentClass } from 'react';
+
+export interface OuterProps {
+  type: string;
+  queries: any[];
+  isVisible: boolean;
+}
+
+export interface PanelProps extends OuterProps {
+  data: any[];
+}
+
+export interface DataPanel extends ComponentClass<OuterProps> {
+}
+
+interface State {
+  isLoading: boolean;
+  data: any[];
+}
+
+export const DataPanelWrapper = (ComposedComponent: ComponentClass<PanelProps>) => {
+  class Wrapper extends Component<OuterProps, State> {
+    public static defaultProps = {
+      isVisible: true,
+    };
+
+    constructor(props: OuterProps) {
+      super(props);
+
+      this.state = {
+        isLoading: false,
+        data: [],
+      };
+    }
+
+    public componentDidMount() {
+      console.log('data panel mount');
+      this.issueQueries();
+    }
+
+    public issueQueries = async () => {
+      const { isVisible } = this.props;
+
+      if (!isVisible) {
+        return;
+      }
+
+      this.setState({ isLoading: true });
+
+      await new Promise(resolve => {
+        setTimeout(() => {
+
+          this.setState({ isLoading: false, data: [{value: 10}] });
+
+        }, 500);
+      });
+    };
+
+    public render() {
+      const { data, isLoading } = this.state;
+      console.log('data panel render');
+
+      if (!data.length) {
+        return (
+          <div className="no-data">
+            <p>No Data</p>
+          </div>
+        );
+      }
+
+      if (isLoading) {
+        return (
+          <div className="loading">
+            <p>Loading</p>
+          </div>
+        );
+      }
+
+      return <ComposedComponent {...this.props} data={data} />;
+    }
+  }
+
+  return Wrapper;
+};
+

+ 30 - 16
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -1,44 +1,58 @@
-import React from 'react';
+import React, { ComponentClass } from 'react';
 import $ from 'jquery';
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
 import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
 import { PanelHeader } from './PanelHeader';
 import { PanelEditor } from './PanelEditor';
+import { DataPanel, PanelProps, DataPanelWrapper } from './DataPanel';
 
 const TITLE_HEIGHT = 27;
 const PANEL_BORDER = 2;
 
-export interface PanelChromeProps {
+export interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
-  component: any;
+  component: ComponentClass<PanelProps>;
 }
 
-export class PanelChrome extends React.Component<PanelChromeProps, any> {
+interface State {
+  height: number;
+}
+
+export class PanelChrome extends React.Component<Props, State> {
+  panelComponent: DataPanel;
+
   constructor(props) {
     super(props);
 
-    this.props.panel.events.on('panel-size-changed', this.triggerForceUpdate.bind(this));
-  }
+    this.state = {
+      height: this.getPanelHeight(),
+    };
 
-  triggerForceUpdate() {
-    this.forceUpdate();
+    this.panelComponent = DataPanelWrapper(this.props.component);
+    this.props.panel.events.on('panel-size-changed', this.onPanelSizeChanged);
   }
 
-  render() {
-    let panelContentStyle = {
+  onPanelSizeChanged = () => {
+    this.setState({
       height: this.getPanelHeight(),
-    };
+    });
+  };
 
-    let PanelComponent = this.props.component;
+  componentDidMount() {
+    console.log('panel chrome mounted');
+  }
+
+  render() {
+    let PanelComponent = this.panelComponent;
 
     return (
-      <div className="panel-height-helper">
+      <div className="panel-editor-container">
         <div className="panel-container">
           <PanelHeader panel={this.props.panel} dashboard={this.props.dashboard} />
-          <div className="panel-content" style={panelContentStyle}>
-            {<PanelComponent />}
+          <div className="panel-content" style={{ height: this.state.height }}>
+            {<PanelComponent type={'test'} queries={[]} isVisible={true} />}
           </div>
         </div>
         {this.props.panel.isEditing && <PanelEditor panel={this.props.panel} dashboard={this.props.dashboard} />}
@@ -59,6 +73,6 @@ export class PanelChrome extends React.Component<PanelChromeProps, any> {
       height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
     }
 
-    return height - PANEL_BORDER + TITLE_HEIGHT;
+    return height - (PANEL_BORDER + TITLE_HEIGHT);
   }
 }

+ 67 - 32
public/app/features/dashboard/dashgrid/PanelEditor.tsx

@@ -1,17 +1,27 @@
 import React from 'react';
+import classNames from 'classnames';
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
-import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader';
+import { store } from 'app/stores/store';
+import { observer } from 'mobx-react';
+import { QueriesTab } from './QueriesTab';
+import { PanelPlugin } from 'app/core/config';
+import { VizTypePicker } from './VizTypePicker';
 
 interface PanelEditorProps {
   panel: PanelModel;
   dashboard: DashboardModel;
 }
 
+interface PanelEditorTab {
+  id: string;
+  text: string;
+  icon: string;
+}
+
+@observer
 export class PanelEditor extends React.Component<PanelEditorProps, any> {
-  queryElement: any;
-  queryComp: AngularComponent;
-  tabs: any[];
+  tabs: PanelEditorTab[];
 
   constructor(props) {
     super(props);
@@ -22,40 +32,42 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
     ];
   }
 
-  componentDidMount() {
-    if (!this.queryElement) {
-      return;
-    }
-
-    let loader = getAngularLoader();
-    var template = '<metrics-tab />';
-    let scopeProps = {
-      ctrl: {
-        panel: this.props.panel,
-        dashboard: this.props.dashboard,
-        panelCtrl: {
-          panel: this.props.panel,
-          dashboard: this.props.dashboard,
-        },
-      },
-    };
-
-    this.queryComp = loader.load(this.queryElement, scopeProps, template);
+  renderQueriesTab() {
+    return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  renderVizTab() {
+    return (
+      <div className="viz-editor">
+        <div className="viz-editor-col1">
+          <VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.onVizTypeChanged} />
+        </div>
+        <div className="viz-editor-col2">
+          <h5 className="page-heading">Options</h5>
+        </div>
+      </div>
+    );
   }
 
-  onChangeTab = tabName => {};
+  onVizTypeChanged = (plugin: PanelPlugin) => {
+    this.props.panel.type = plugin.id;
+    this.forceUpdate();
+  };
+
+  onChangeTab = (tab: PanelEditorTab) => {
+    store.view.updateQuery({ tab: tab.id }, false);
+  };
 
   render() {
+    const activeTab: string = store.view.query.get('tab') || 'queries';
+
     return (
-      <div className="tabbed-view tabbed-view--panel-edit-new">
+      <div className="tabbed-view tabbed-view--new">
         <div className="tabbed-view-header">
           <ul className="gf-tabs">
-            <li className="gf-tabs-item">
-              <a className="gf-tabs-link active">Queries</a>
-            </li>
-            <li className="gf-tabs-item">
-              <a className="gf-tabs-link">Visualization</a>
-            </li>
+            {this.tabs.map(tab => {
+              return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
+            })}
           </ul>
 
           <button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
@@ -64,9 +76,32 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
         </div>
 
         <div className="tabbed-view-body">
-          <div ref={element => (this.queryElement = element)} className="panel-height-helper" />
+          {activeTab === 'queries' && this.renderQueriesTab()}
+          {activeTab === 'viz' && this.renderVizTab()}
         </div>
       </div>
     );
   }
 }
+
+interface TabItemParams {
+  tab: PanelEditorTab;
+  activeTab: string;
+  onClick: (tab: PanelEditorTab) => void;
+}
+
+function TabItem({ tab, activeTab, onClick }: TabItemParams) {
+  const tabClasses = classNames({
+    'gf-tabs-link': true,
+    active: activeTab === tab.id,
+  });
+
+  return (
+    <li className="gf-tabs-item" key={tab.id}>
+      <a className={tabClasses} onClick={() => onClick(tab)}>
+        <i className={tab.icon} />
+        {tab.text}
+      </a>
+    </li>
+  );
+}

+ 8 - 5
public/app/features/dashboard/dashgrid/PanelHeader.tsx

@@ -11,11 +11,14 @@ interface PanelHeaderProps {
 
 export class PanelHeader extends React.Component<PanelHeaderProps, any> {
   onEditPanel = () => {
-    store.view.updateQuery({
-      panelId: this.props.panel.id,
-      edit: true,
-      fullscreen: true,
-    });
+    store.view.updateQuery(
+      {
+        panelId: this.props.panel.id,
+        edit: true,
+        fullscreen: true,
+      },
+      false
+    );
   };
 
   render() {

+ 49 - 0
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader';
+
+interface Props {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+}
+
+export class QueriesTab extends React.Component<Props, any> {
+  element: any;
+  component: AngularComponent;
+
+  constructor(props) {
+    super(props);
+  }
+
+  componentDidMount() {
+    if (!this.element) {
+      return;
+    }
+
+    let loader = getAngularLoader();
+    var template = '<metrics-tab />';
+    let scopeProps = {
+      ctrl: {
+        panel: this.props.panel,
+        dashboard: this.props.dashboard,
+        panelCtrl: {
+          panel: this.props.panel,
+          dashboard: this.props.dashboard,
+        },
+      },
+    };
+
+    this.component = loader.load(this.element, scopeProps, template);
+  }
+
+  componentWillUnmount() {
+    if (this.component) {
+      this.component.destroy();
+    }
+  }
+
+  render() {
+    return <div ref={element => (this.element = element)} className="panel-height-helper" />;
+  }
+}

+ 61 - 0
public/app/features/dashboard/dashgrid/VizTypePicker.tsx

@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import config, { PanelPlugin } from 'app/core/config';
+import _ from 'lodash';
+
+interface Props {
+  currentType: string;
+  onTypeChanged: (newType: PanelPlugin) => void;
+}
+
+interface State {
+  pluginList: PanelPlugin[];
+}
+
+export class VizTypePicker extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pluginList: this.getPanelPlugins(''),
+    };
+  }
+
+  getPanelPlugins(filter) {
+    let panels = _.chain(config.panels)
+      .filter({ hideFromList: false })
+      .map(item => item)
+      .value();
+
+    // add sort by sort property
+    return _.sortBy(panels, 'sort');
+  }
+
+  renderVizPlugin = (plugin, index) => {
+    const cssClass = classNames({
+      'viz-picker__item': true,
+      'viz-picker__item--selected': plugin.id === this.props.currentType,
+    });
+
+    return (
+      <div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
+        <img className="viz-picker__item-img" src={plugin.info.logos.small} />
+        <div className="viz-picker__item-name">{plugin.name}</div>
+      </div>
+    );
+  };
+
+  render() {
+    return (
+      <div className="viz-picker">
+        <div className="gf-form gf-form--grow">
+          <label className="gf-form--has-input-icon gf-form--grow">
+            <input type="text" className="gf-form-input" placeholder="Search type" />
+            <i className="gf-form-input-icon fa fa-search" />
+          </label>
+        </div>
+        <div className="viz-picker-list">{this.state.pluginList.map(this.renderVizPlugin)}</div>
+      </div>
+    );
+  }
+}

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

@@ -38,6 +38,8 @@ export class DashNavCtrl {
     } else if (search.fullscreen) {
       delete search.fullscreen;
       delete search.edit;
+      delete search.tab;
+      delete search.panelId;
     }
     this.$location.search(search);
   }

+ 15 - 0
public/app/features/dashboard/specs/AddPanelPanel.jest.tsx

@@ -23,6 +23,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Singlestat',
         sort: 2,
+        module: '',
+        baseUrl: '',
+        meta: {},
         info: {
           logos: {
             small: '',
@@ -34,6 +37,9 @@ describe('AddPanelPanel', () => {
         hideFromList: true,
         name: 'Hidden',
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -45,6 +51,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Graph',
         sort: 1,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -56,6 +65,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Zabbix',
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -67,6 +79,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Piechart',
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',

+ 1 - 1
public/app/features/panel/panel_directive.ts

@@ -26,7 +26,7 @@ var panelTemplate = `
   </div>
 
   <div class="panel-full-edit" ng-if="ctrl.panel.isEditing">
-    <div class="tabbed-view tabbed-view--panel-edit">
+    <div class="tabbed-view">
       <div class="tabbed-view-header">
         <h3 class="tabbed-view-panel-title">
           {{ctrl.pluginName}}

+ 1 - 2
public/app/features/plugins/plugin_component.ts

@@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
 
       PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
         PanelCtrl.templateUrl = null;
-        PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-height-helper">${template}</grafana-panel>`;
+        PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-editor-container">${template}</grafana-panel>`;
         return componentInfo;
       });
 
@@ -110,7 +110,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
         let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
         return datasourceSrv.get(datasource).then(ds => {
           scope.datasource = ds;
-          console.log('scope', scope);
 
           return importPluginModule(ds.meta.module).then(dsModule => {
             return {

+ 12 - 1
public/app/features/plugins/plugin_loader.ts

@@ -138,11 +138,22 @@ const flotDeps = [
   'jquery.flot.stackpercent',
   'jquery.flot.events',
 ];
+
 for (let flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });
 }
 
-export function importPluginModule(path: string): Promise<any> {
+export interface PluginExports {
+  PanelCtrl?;
+  any;
+  PanelComponent?: any;
+  Datasource?: any;
+  QueryCtrl?: any;
+  ConfigCtrl?: any;
+  AnnotationsQueryCtrl?: any;
+}
+
+export function importPluginModule(path: string): Promise<PluginExports> {
   let builtIn = builtInPlugins[path];
   if (builtIn) {
     return Promise.resolve(builtIn);

+ 11 - 3
public/app/plugins/panel/text2/module.tsx

@@ -1,12 +1,20 @@
-import React from 'react';
+import React, { PureComponent } from 'react';
+import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel';
 
-export class ReactTestPanel extends React.Component<any, any> {
+export class ReactTestPanel extends PureComponent<PanelProps> {
   constructor(props) {
     super(props);
   }
 
   render() {
-    return <h2>I am a react panel, haha!</h2>;
+    const { data } = this.props;
+    let value = 0;
+
+    if (data.length) {
+      value = data[0].value;
+    }
+
+    return <h2>I am a react value: {value}</h2>;
   }
 }
 

+ 4 - 2
public/app/stores/ViewStore/ViewStore.ts

@@ -23,8 +23,10 @@ export const ViewStore = types
   }))
   .actions(self => {
     // querystring only
-    function updateQuery(query: any) {
-      self.query.clear();
+    function updateQuery(query: any, clear = true) {
+      if (clear) {
+        self.query.clear();
+      }
       for (let key of Object.keys(query)) {
         if (query[key]) {
           self.query.set(key, query[key]);

+ 1 - 0
public/sass/_grafana.scss

@@ -93,6 +93,7 @@
 @import 'components/form_select_box';
 @import 'components/user-picker';
 @import 'components/description-picker';
+@import 'components/viz_editor';
 
 // PAGES
 @import 'pages/login';

+ 0 - 4
public/sass/components/_panel_add_panel.scss

@@ -85,10 +85,6 @@
   height: calc(100% - 15px);
 }
 
-.add-panel__item-icon {
-  padding: 2px;
-}
-
 .add-panel__searchbar {
   width: 100%;
   margin-bottom: 10px;

+ 9 - 19
public/sass/components/_tabbed_view.scss

@@ -1,28 +1,15 @@
 .tabbed-view {
-  padding: $spacer*3;
-  margin-bottom: $dashboard-padding;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
 
-  &.tabbed-view--panel-edit {
-    padding: 0;
-
-    .tabbed-view-header {
-      padding: 0px 25px;
-      background: none;
-    }
-  }
-
-  &.tabbed-view--panel-edit-new {
+  &.tabbed-view--new {
     padding: 10px 0 0 0;
-
-    .tabbed-view-header {
-      padding: 0px;
-      background: none;
-    }
+    height: 100%;
   }
 }
 
 .tabbed-view-header {
-  background: $page-header-bg;
   box-shadow: $page-header-shadow;
   border-bottom: 1px solid $page-header-border-color;
   @include clearfix();
@@ -57,7 +44,10 @@
 }
 
 .tabbed-view-body {
-  padding: $spacer*2 $spacer;
+  padding: $spacer*2 $spacer $spacer $spacer;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
 
   &--small {
     min-height: 0px;

+ 78 - 0
public/sass/components/_viz_editor.scss

@@ -0,0 +1,78 @@
+.viz-editor {
+  display: flex;
+  height: 100%;
+}
+
+.viz-editor-col1 {
+  width: 210px;
+  height: 100%;
+  margin-right: 40px;
+}
+
+.viz-editor-col2 {
+  flex-grow: 1;
+}
+
+.viz-picker {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.viz-picker-list {
+  padding-top: $spacer;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  flex-grow: 1;
+}
+
+.viz-picker__item {
+  background: $card-background;
+  box-shadow: $card-shadow;
+
+  border-radius: 3px;
+  padding: $spacer;
+  width: 100%;
+  height: 60px;
+  text-align: center;
+  margin-bottom: 6px;
+  cursor: pointer;
+  display: flex;
+  flex-shrink: 0;
+  border: 1px solid transparent;
+
+  &:hover {
+    background: $card-background-hover;
+  }
+
+  &--selected {
+    border: 1px solid $orange;
+
+    .viz-picker__item-name {
+      color: $text-color;
+    }
+
+    .viz-picker__item-img {
+      filter: saturate(100%);
+    }
+  }
+}
+
+.viz-picker__item-name {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  font-size: $font-size-h5;
+  display: flex;
+  flex-direction: column;
+  align-self: center;
+  padding-left: $spacer;
+  font-size: $font-size-md;
+  color: $text-muted;
+}
+
+.viz-picker__item-img {
+  height: 100%;
+  filter: saturate(30%);
+}

+ 9 - 3
public/sass/pages/_dashboard.scss

@@ -1,7 +1,8 @@
 .dashboard-container {
   padding: $dashboard-padding;
   width: 100%;
-  min-height: 100%;
+  height: 100%;
+  box-sizing: border-box;
 }
 
 .template-variable {
@@ -28,12 +29,17 @@ div.flot-text {
   height: 100%;
 }
 
+.panel-editor-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
 .panel-container {
   background-color: $panel-bg;
   border: $panel-border;
   position: relative;
   border-radius: 3px;
-  height: 100%;
 
   &.panel-transparent {
     background-color: transparent;
@@ -233,5 +239,5 @@ div.flot-text {
 }
 
 .panel-full-edit {
-  margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding);
+  padding-top: $dashboard-padding;
 }