Browse Source

Merge pull request #14476 from grafana/metrics-tab-changes-2

Metrics tab changes 2
Torkel Ödegaard 7 years ago
parent
commit
a9e98feea0
32 changed files with 595 additions and 551 deletions
  1. 7 6
      public/app/core/components/Picker/DescriptionOption.tsx
  2. 2 2
      public/app/core/components/Picker/PickerOption.test.tsx
  3. 29 7
      public/app/core/components/Picker/PickerOption.tsx
  4. 60 0
      public/app/core/components/Picker/Select.tsx
  5. 2 0
      public/app/core/components/Picker/SimplePicker.tsx
  6. 1 1
      public/app/core/components/Picker/TeamPicker.tsx
  7. 1 1
      public/app/core/components/Picker/UserPicker.tsx
  8. 9 5
      public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
  9. 2 1
      public/app/features/dashboard/dashboard_migration.ts
  10. 0 10
      public/app/features/dashboard/dashboard_model.ts
  11. 54 88
      public/app/features/dashboard/dashgrid/DataSourcePicker.tsx
  12. 15 17
      public/app/features/dashboard/dashgrid/EditorTabBody.tsx
  13. 172 85
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  14. 92 22
      public/app/features/dashboard/dashgrid/VisualizationTab.tsx
  15. 10 70
      public/app/features/dashboard/dashgrid/VizTypePicker.tsx
  16. 4 7
      public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx
  17. 19 0
      public/app/features/dashboard/panel_model.ts
  18. 9 167
      public/app/features/panel/metrics_tab.ts
  19. 17 20
      public/app/features/panel/partials/metrics_tab.html
  20. 1 1
      public/app/features/panel/query_editor_row.ts
  21. 2 2
      public/app/plugins/datasource/prometheus/partials/query.editor.html
  22. 0 2
      public/sass/_grafana.scss
  23. 4 3
      public/sass/_variables.dark.scss
  24. 1 0
      public/sass/_variables.light.scss
  25. 1 0
      public/sass/components/_buttons.scss
  26. 0 11
      public/sass/components/_description-picker.scss
  27. 42 2
      public/sass/components/_form_select_box.scss
  28. 0 1
      public/sass/components/_gf-form.scss
  29. 5 3
      public/sass/components/_json_explorer.scss
  30. 30 5
      public/sass/components/_panel_editor.scss
  31. 0 12
      public/sass/components/_user-picker.scss
  32. 4 0
      public/sass/utils/_utils.scss

+ 7 - 6
public/app/core/components/Picker/DescriptionOption.tsx

@@ -8,15 +8,16 @@ interface ExtendedOptionProps extends OptionProps<any> {
 }
 
 export const Option = (props: ExtendedOptionProps) => {
-  const { children, isSelected, data, className } = props;
+  const { children, isSelected, data } = props;
+
   return (
     <components.Option {...props}>
-      <div className={`description-picker-option__button btn btn-link ${className}`}>
-        {isSelected && <i className="fa fa-check pull-right" aria-hidden="true" />}
-        <div className="gf-form">{children}</div>
-        <div className="gf-form">
-          <div className="muted width-17">{data.description}</div>
+      <div className="gf-form-select-box__desc-option">
+        <div className="gf-form-select-box__desc-option__body">
+          <div>{children}</div>
+          {data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
         </div>
+        {isSelected && <i className="fa fa-check" aria-hidden="true" />}
       </div>
     </components.Option>
   );

+ 2 - 2
public/app/core/components/Picker/PickerOption.test.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React from 'react';
 import renderer from 'react-test-renderer';
 import PickerOption from './PickerOption';
 
@@ -24,7 +24,7 @@ const model = {
   children: 'Model title',
   data: {
     title: 'Model title',
-    avatarUrl: 'url/to/avatar',
+    imgUrl: 'url/to/avatar',
     label: 'User picker label',
   },
   className: 'class-for-user-picker',

+ 29 - 7
public/app/core/components/Picker/PickerOption.tsx

@@ -4,19 +4,41 @@ import { OptionProps } from 'react-select/lib/components/Option';
 
 // https://github.com/JedWatson/react-select/issues/3038
 interface ExtendedOptionProps extends OptionProps<any> {
-  data: any;
+  data: {
+    description?: string;
+    imgUrl?: string;
+  };
 }
 
-export const PickerOption = (props: ExtendedOptionProps) => {
-  const { children, data, className } = props;
+export const Option = (props: ExtendedOptionProps) => {
+  const { children, isSelected, data } = props;
+
   return (
     <components.Option {...props}>
-      <div className={`description-picker-option__button btn btn-link ${className}`}>
-        {data.avatarUrl && <img src={data.avatarUrl} alt={data.label} className="user-picker-option__avatar" />}
-        {children}
+      <div className="gf-form-select-box__desc-option">
+        {data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
+        <div className="gf-form-select-box__desc-option__body">
+          <div>{children}</div>
+          {data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
+        </div>
+        {isSelected && <i className="fa fa-check" aria-hidden="true" />}
       </div>
     </components.Option>
   );
 };
 
-export default PickerOption;
+// was not able to type this without typescript error
+export const SingleValue = props => {
+  const { children, data } = props;
+
+  return (
+    <components.SingleValue {...props}>
+      <div className="gf-form-select-box__img-value">
+        {data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
+        {children}
+      </div>
+    </components.SingleValue>
+  );
+};
+
+export default Option;

+ 60 - 0
public/app/core/components/Picker/Select.tsx

@@ -0,0 +1,60 @@
+// import React, { PureComponent } from 'react';
+// import Select as ReactSelect from 'react-select';
+// import DescriptionOption from './DescriptionOption';
+// import IndicatorsContainer from './IndicatorsContainer';
+// import ResetStyles from './ResetStyles';
+//
+// export interface OptionType {
+//   label: string;
+//   value: string;
+// }
+//
+// interface Props {
+//   defaultValue?: any;
+//   getOptionLabel: (item: T) => string;
+//   getOptionValue: (item: T) => string;
+//   onChange: (item: T) => {} | void;
+//   options: T[];
+//   placeholder?: string;
+//   width?: number;
+//   value: T;
+//   className?: string;
+// }
+//
+// export class Select<T> extends PureComponent<Props<T>> {
+//   static defaultProps = {
+//     width: null,
+//     className: '',
+//   }
+//
+//   render() {
+//     const { defaultValue, getOptionLabel, getOptionValue, onSelected, options, placeholder, width, value, className } = this.props;
+//     let widthClass = '';
+//     if (width) {
+//       widthClass = 'width-'+width;
+//     }
+//
+//   return (
+//     <ReactSelect
+//       classNamePrefix="gf-form-select-box"
+//       className={`gf-form-input gf-form-input--form-dropdown ${widthClass} ${className}`}
+//       components={{
+//         Option: DescriptionOption,
+//         IndicatorsContainer,
+//       }}
+//       defaultValue={defaultValue}
+//       value={value}
+//       getOptionLabel={getOptionLabel}
+//       getOptionValue={getOptionValue}
+//       menuShouldScrollIntoView={false}
+//       isSearchable={false}
+//       onChange={onSelected}
+//       options={options}
+//       placeholder={placeholder || 'Choose'}
+//       styles={ResetStyles}
+//     />
+//   );
+// }
+// }
+//
+// export default Select;

+ 2 - 0
public/app/core/components/Picker/SimplePicker.tsx

@@ -1,6 +1,7 @@
 import React, { SFC } from 'react';
 import Select from 'react-select';
 import DescriptionOption from './DescriptionOption';
+import IndicatorsContainer from './IndicatorsContainer';
 import ResetStyles from './ResetStyles';
 
 interface Props {
@@ -32,6 +33,7 @@ const SimplePicker: SFC<Props> = ({
       className={`${width ? 'width-' + width : ''} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
       components={{
         Option: DescriptionOption,
+        IndicatorsContainer,
       }}
       defaultValue={defaultValue}
       value={value}

+ 1 - 1
public/app/core/components/Picker/TeamPicker.tsx

@@ -47,7 +47,7 @@ export class TeamPicker extends Component<Props, State> {
           id: team.id,
           label: team.name,
           name: team.name,
-          avatarUrl: team.avatarUrl,
+          imgUrl: team.avatarUrl,
         };
       });
 

+ 1 - 1
public/app/core/components/Picker/UserPicker.tsx

@@ -41,7 +41,7 @@ export class UserPicker extends Component<Props, State> {
         return result.map(user => ({
           id: user.userId,
           label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
-          avatarUrl: user.avatarUrl,
+          imgUrl: user.avatarUrl,
           login: user.login,
         }));
       })

+ 9 - 5
public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap

@@ -3,15 +3,19 @@
 exports[`PickerOption renders correctly 1`] = `
 <div>
   <div
-    className="description-picker-option__button btn btn-link class-for-user-picker"
+    className="gf-form-select-box__desc-option"
   >
     <img
-      alt="User picker label"
-      className="user-picker-option__avatar"
+      className="gf-form-select-box__desc-option__img"
       src="url/to/avatar"
     />
-    Model title
+    <div
+      className="gf-form-select-box__desc-option__body"
+    >
+      <div>
+        Model title
+      </div>
+    </div>
   </div>
 </div>
 `;
-  

+ 2 - 1
public/app/features/dashboard/dashboard_migration.ts

@@ -141,9 +141,10 @@ export class DashboardMigrator {
 
       // ensure query refIds
       panelUpgrades.push(panel => {
+        console.log('asdasd', panel);
         _.each(panel.targets, target => {
           if (!target.refId) {
-            target.refId = this.dashboard.getNextQueryLetter(panel);
+            target.refId = panel.getNextQueryLetter && panel.getNextQueryLetter();
           }
         });
       });

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

@@ -806,16 +806,6 @@ export class DashboardModel {
     return this.timezone === 'browser' ? moment(date).fromNow() : moment.utc(date).fromNow();
   }
 
-  getNextQueryLetter(panel) {
-    const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    return _.find(letters, refId => {
-      return _.every(panel.targets, other => {
-        return other.refId !== refId;
-      });
-    });
-  }
-
   isTimezoneUtc() {
     return this.getTimezone() === 'utc';
   }

+ 54 - 88
public/app/features/dashboard/dashgrid/DataSourcePicker.tsx

@@ -1,115 +1,81 @@
+// Libraries
 import React, { PureComponent } from 'react';
-import classNames from 'classnames';
 import _ from 'lodash';
-import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation';
+
+// Components
+import ResetStyles from 'app/core/components/Picker/ResetStyles';
+import { Option, SingleValue } from 'app/core/components/Picker/PickerOption';
+import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
+import Select from 'react-select';
+
+// Types
 import { DataSourceSelectItem } from 'app/types';
 
 export interface Props {
   onChangeDataSource: (ds: DataSourceSelectItem) => void;
   datasources: DataSourceSelectItem[];
+  current: DataSourceSelectItem;
+  onBlur?: () => void;
+  autoFocus?: boolean;
 }
 
-interface State {
-  searchQuery: string;
-}
+export class DataSourcePicker extends PureComponent<Props> {
+  static defaultProps = {
+    autoFocus: false,
+  };
 
-export class DataSourcePicker extends PureComponent<Props, State> {
   searchInput: HTMLElement;
 
   constructor(props) {
     super(props);
-    this.state = {
-      searchQuery: '',
-    };
   }
 
-  getDataSources() {
-    const { searchQuery } = this.state;
-    const regex = new RegExp(searchQuery, 'i');
-    const { datasources } = this.props;
-
-    const filtered = datasources.filter(item => {
-      return regex.test(item.name) || regex.test(item.meta.name);
-    });
-
-    return filtered;
-  }
-
-  get maxSelectedIndex() {
-    const filtered = this.getDataSources();
-    return filtered.length - 1;
-  }
-
-  renderDataSource = (ds: DataSourceSelectItem, index: number, keyNavProps: KeyboardNavigationProps) => {
-    const { onChangeDataSource } = this.props;
-    const { selected, onMouseEnter } = keyNavProps;
-    const onClick = () => onChangeDataSource(ds);
-    const isSelected = selected === index;
-    const cssClass = classNames({
-      'ds-picker-list__item': true,
-      'ds-picker-list__item--selected': isSelected,
-    });
-    return (
-      <div key={index} className={cssClass} title={ds.name} onClick={onClick} onMouseEnter={() => onMouseEnter(index)}>
-        <img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
-        <div className="ds-picker-list__name">{ds.name}</div>
-      </div>
-    );
+  onChange = item => {
+    const ds = this.props.datasources.find(ds => ds.name === item.value);
+    this.props.onChangeDataSource(ds);
   };
 
-  componentDidMount() {
-    setTimeout(() => {
-      this.searchInput.focus();
-    }, 300);
-  }
+  render() {
+    const { datasources, current, autoFocus, onBlur } = this.props;
 
-  onSearchQueryChange = evt => {
-    const value = evt.target.value;
-    this.setState(prevState => ({
-      ...prevState,
-      searchQuery: value,
+    const options = datasources.map(ds => ({
+      value: ds.name,
+      label: ds.name,
+      imgUrl: ds.meta.info.logos.small,
     }));
-  };
 
-  renderFilters({ onKeyDown, selected }: KeyboardNavigationProps) {
-    const { searchQuery } = this.state;
+    const value = current && {
+      label: current.name,
+      value: current.name,
+      imgUrl: current.meta.info.logos.small,
+    };
+
     return (
-      <label className="gf-form--has-input-icon">
-        <input
-          type="text"
-          className="gf-form-input width-13"
-          placeholder=""
-          ref={elem => (this.searchInput = elem)}
-          onChange={this.onSearchQueryChange}
-          value={searchQuery}
-          onKeyDown={evt => {
-            onKeyDown(evt, this.maxSelectedIndex, () => {
-              const { onChangeDataSource } = this.props;
-              const ds = this.getDataSources()[selected];
-              onChangeDataSource(ds);
-            });
+      <div className="gf-form-inline">
+        <Select
+          classNamePrefix={`gf-form-select-box`}
+          isMulti={false}
+          menuShouldScrollIntoView={false}
+          isClearable={false}
+          className="gf-form-input gf-form-input--form-dropdown ds-picker"
+          onChange={item => this.onChange(item)}
+          options={options}
+          styles={ResetStyles}
+          autoFocus={autoFocus}
+          onBlur={onBlur}
+          openMenuOnFocus={true}
+          maxMenuHeight={500}
+          placeholder="Select datasource"
+          loadingMessage={() => 'Loading datasources...'}
+          noOptionsMessage={() => 'No datasources found'}
+          value={value}
+          components={{
+            Option,
+            SingleValue,
+            IndicatorsContainer,
           }}
         />
-        <i className="gf-form-input-icon fa fa-search" />
-      </label>
-    );
-  }
-
-  render() {
-    return (
-      <KeyboardNavigation
-        render={(keyNavProps: KeyboardNavigationProps) => (
-          <>
-            <div className="cta-form__bar">
-              {this.renderFilters(keyNavProps)}
-              <div className="gf-form--grow" />
-            </div>
-            <div className="ds-picker-list">
-              {this.getDataSources().map((ds, index) => this.renderDataSource(ds, index, keyNavProps))}
-            </div>
-          </>
-        )}
-      />
+      </div>
     );
   }
 }

+ 15 - 17
public/app/features/dashboard/dashgrid/EditorTabBody.tsx

@@ -5,8 +5,8 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn';
 interface Props {
   children: JSX.Element;
   heading: string;
-  main?: EditorToolBarView;
-  toolbarItems: EditorToolBarView[];
+  renderToolbar?: () => JSX.Element;
+  toolbarItems?: EditorToolBarView[];
 }
 
 export interface EditorToolBarView {
@@ -15,7 +15,7 @@ export interface EditorToolBarView {
   icon?: string;
   disabled?: boolean;
   onClick?: () => void;
-  render: (closeFunction: any) => JSX.Element | JSX.Element[];
+  render: (closeFunction?: any) => JSX.Element | JSX.Element[];
 }
 
 interface State {
@@ -25,6 +25,10 @@ interface State {
 }
 
 export class EditorTabBody extends PureComponent<Props, State> {
+  static defaultProps = {
+    toolbarItems: [],
+  };
+
   constructor(props) {
     super(props);
 
@@ -65,16 +69,6 @@ export class EditorTabBody extends PureComponent<Props, State> {
     return state;
   }
 
-  renderMainSelection(view: EditorToolBarView) {
-    return (
-      <div className="toolbar__main" onClick={() => this.onToggleToolBarView(view)} key={view.title + view.icon}>
-        <img className="toolbar__main-image" src={view.imgSrc} />
-        <div className="toolbar__main-name">{view.title}</div>
-        <i className="fa fa-caret-down" />
-      </div>
-    );
-  }
-
   renderButton(view: EditorToolBarView) {
     const onClick = () => {
       if (view.onClick) {
@@ -104,16 +98,20 @@ export class EditorTabBody extends PureComponent<Props, State> {
   }
 
   render() {
-    const { children, toolbarItems, main, heading } = this.props;
+    const { children, renderToolbar, heading, toolbarItems } = this.props;
     const { openView, fadeIn, isOpen } = this.state;
 
     return (
       <>
         <div className="toolbar">
           <div className="toolbar__heading">{heading}</div>
-          {main && this.renderMainSelection(main)}
-          <div className="gf-form--grow" />
-          {toolbarItems.map(item => this.renderButton(item))}
+          {renderToolbar && renderToolbar()}
+          {toolbarItems.length > 0 && (
+            <>
+              <div className="gf-form--grow" />
+              {toolbarItems.map(item => this.renderButton(item))}
+            </>
+          )}
         </div>
         <div className="panel-editor__scroll">
           <CustomScrollbar autoHide={false}>

+ 172 - 85
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -1,36 +1,39 @@
+// Libraries
 import React, { SFC, PureComponent } from 'react';
+import Remarkable from 'remarkable';
+import _ from 'lodash';
+
+// Components
 import DataSourceOption from './DataSourceOption';
-import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
 import { EditorTabBody } from './EditorTabBody';
 import { DataSourcePicker } from './DataSourcePicker';
-import { PanelModel } from '../panel_model';
-import { DashboardModel } from '../dashboard_model';
-import './../../panel/metrics_tab';
-import config from 'app/core/config';
 import { QueryInspector } from './QueryInspector';
 import { TimeRangeOptions } from './TimeRangeOptions';
+import './../../panel/metrics_tab';
+import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab';
 
 // Services
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
-import { DataSourceSelectItem } from 'app/types';
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import config from 'app/core/config';
 
-import Remarkable from 'remarkable';
+// Types
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { DataSourceSelectItem, DataQuery } from 'app/types';
 
 interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
 }
 
-interface Help {
-  isLoading: boolean;
-  helpHtml: any;
-}
-
 interface State {
-  currentDatasource: DataSourceSelectItem;
-  help: Help;
-  hideTimeOverride: boolean;
+  currentDS: DataSourceSelectItem;
+  helpContent: JSX.Element;
+  isLoadingHelp: boolean;
+  isPickerOpen: boolean;
+  isAddingMixed: boolean;
 }
 
 interface LoadingPlaceholderProps {
@@ -40,7 +43,7 @@ interface LoadingPlaceholderProps {
 const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
 
 export class QueriesTab extends PureComponent<Props, State> {
-  element: any;
+  element: HTMLElement;
   component: AngularComponent;
   datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
   backendSrv: BackendSrv = getBackendSrv();
@@ -50,12 +53,26 @@ export class QueriesTab extends PureComponent<Props, State> {
     const { panel } = props;
 
     this.state = {
-      currentDatasource: this.datasources.find(datasource => datasource.value === panel.datasource),
-      help: {
-        isLoading: false,
-        helpHtml: null,
-      },
-      hideTimeOverride: false,
+      currentDS: this.datasources.find(datasource => datasource.value === panel.datasource),
+      isLoadingHelp: false,
+      helpContent: null,
+      isPickerOpen: false,
+      isAddingMixed: false,
+    };
+  }
+
+  getAngularQueryComponentScope(): AngularQueryComponentScope {
+    const { panel, dashboard } = this.props;
+
+    return {
+      panel: panel,
+      dashboard: dashboard,
+      refresh: () => panel.refresh(),
+      render: () => panel.render,
+      addQuery: this.onAddQuery,
+      moveQuery: this.onMoveQuery,
+      removeQuery: this.onRemoveQuery,
+      events: panel.events,
     };
   }
 
@@ -64,15 +81,10 @@ export class QueriesTab extends PureComponent<Props, State> {
       return;
     }
 
-    const { panel, dashboard } = this.props;
     const loader = getAngularLoader();
     const template = '<metrics-tab />';
     const scopeProps = {
-      ctrl: {
-        panel: panel,
-        dashboard: dashboard,
-        refresh: () => panel.refresh(),
-      },
+      ctrl: this.getAngularQueryComponentScope(),
     };
 
     this.component = loader.load(this.element, scopeProps, template);
@@ -86,7 +98,8 @@ export class QueriesTab extends PureComponent<Props, State> {
 
   onChangeDataSource = datasource => {
     const { panel } = this.props;
-    const { currentDatasource } = this.state;
+    const { currentDS } = this.state;
+
     // switching to mixed
     if (datasource.meta.mixed) {
       panel.targets.forEach(target => {
@@ -95,62 +108,58 @@ export class QueriesTab extends PureComponent<Props, State> {
           target.datasource = config.defaultDatasource;
         }
       });
-    } else if (currentDatasource && currentDatasource.meta.mixed) {
-      panel.targets.forEach(target => {
-        delete target.datasource;
-      });
+    } else if (currentDS) {
+      // if switching from mixed
+      if (currentDS.meta.mixed) {
+        for (const target of panel.targets) {
+          delete target.datasource;
+        }
+      } else if (currentDS.meta.id !== datasource.meta.id) {
+        // we are changing data source type, clear queries
+        panel.targets = [{ refId: 'A' }];
+      }
     }
 
     panel.datasource = datasource.value;
     panel.refresh();
 
-    this.setState(prevState => ({
-      ...prevState,
-      currentDatasource: datasource,
-    }));
+    this.setState({
+      currentDS: datasource,
+    });
   };
 
   loadHelp = () => {
-    const { currentDatasource } = this.state;
-    const hasHelp = currentDatasource.meta.hasQueryHelp;
+    const { currentDS } = this.state;
+    const hasHelp = currentDS.meta.hasQueryHelp;
 
     if (hasHelp) {
-      this.setState(prevState => ({
-        ...prevState,
-        help: {
-          helpHtml: <h2>Loading help...</h2>,
-          isLoading: true,
-        },
-      }));
+      this.setState({
+        helpContent: <h3>Loading help...</h3>,
+        isLoadingHelp: true,
+      });
 
       this.backendSrv
-        .get(`/api/plugins/${currentDatasource.meta.id}/markdown/query_help`)
+        .get(`/api/plugins/${currentDS.meta.id}/markdown/query_help`)
         .then(res => {
           const md = new Remarkable();
-          const helpHtml = md.render(res); // TODO: Clean out dangerous code? Previous: this.helpHtml = this.$sce.trustAsHtml(md.render(res));
-          this.setState(prevState => ({
-            ...prevState,
-            help: {
-              helpHtml: <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />,
-              isLoading: false,
-            },
-          }));
+          const helpHtml = md.render(res);
+          this.setState({
+            helpContent: <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />,
+            isLoadingHelp: false,
+          });
         })
         .catch(() => {
-          this.setState(prevState => ({
-            ...prevState,
-            help: {
-              helpHtml: 'Error occured when loading help',
-              isLoading: false,
-            },
-          }));
+          this.setState({
+            helpContent: <h3>'Error occured when loading help'</h3>,
+            isLoadingHelp: false,
+          });
         });
     }
   };
 
   renderOptions = close => {
-    const { currentDatasource } = this.state;
-    const { queryOptions } = currentDatasource.meta;
+    const { currentDS } = this.state;
+    const { queryOptions } = currentDS.meta;
     const { panel } = this.props;
 
     const onChangeFn = (panelKey: string) => {
@@ -223,26 +232,83 @@ export class QueriesTab extends PureComponent<Props, State> {
   };
 
   renderHelp = () => {
-    const { helpHtml, isLoading } = this.state.help;
-    return isLoading ? <LoadingPlaceholder text="Loading help..." /> : helpHtml;
+    const { helpContent, isLoadingHelp } = this.state;
+    return isLoadingHelp ? <LoadingPlaceholder text="Loading help..." /> : helpContent;
+  };
+
+  onAddQuery = (query?: Partial<DataQuery>) => {
+    this.props.panel.addQuery(query);
+    this.forceUpdate();
+  };
+
+  onAddQueryClick = () => {
+    if (this.state.currentDS.meta.mixed) {
+      this.setState({ isAddingMixed: true });
+      return;
+    }
+
+    this.props.panel.addQuery();
+    this.component.digest();
+    this.forceUpdate();
+  };
+
+  onRemoveQuery = (query: DataQuery) => {
+    const { panel } = this.props;
+
+    const index = _.indexOf(panel.targets, query);
+    panel.targets.splice(index, 1);
+    panel.refresh();
+
+    this.forceUpdate();
+  };
+
+  onMoveQuery = (query: DataQuery, direction: number) => {
+    const { panel } = this.props;
+
+    const index = _.indexOf(panel.targets, query);
+    _.move(panel.targets, index, index + direction);
+
+    this.forceUpdate();
+  };
+
+  renderToolbar = () => {
+    const { currentDS } = this.state;
+
+    return (
+      <DataSourcePicker
+        datasources={this.datasources}
+        onChangeDataSource={this.onChangeDataSource}
+        current={currentDS}
+      />
+    );
+  };
+
+  renderMixedPicker = () => {
+    return (
+      <DataSourcePicker
+        datasources={this.datasources}
+        onChangeDataSource={this.onAddMixedQuery}
+        current={null}
+        autoFocus={true}
+        onBlur={this.onMixedPickerBlur}
+      />
+    );
+  };
+
+  onAddMixedQuery = datasource => {
+    this.onAddQuery({ datasource: datasource.name });
+    this.component.digest();
+    this.setState({ isAddingMixed: false });
+  };
+
+  onMixedPickerBlur = () => {
+    this.setState({ isAddingMixed: false });
   };
 
   render() {
-    const { currentDatasource } = this.state;
-    const { hasQueryHelp } = currentDatasource.meta;
-    const dsInformation = {
-      title: currentDatasource.name,
-      imgSrc: currentDatasource.meta.info.logos.small,
-      render: closeOpenView => (
-        <DataSourcePicker
-          datasources={this.datasources}
-          onChangeDataSource={ds => {
-            closeOpenView();
-            this.onChangeDataSource(ds);
-          }}
-        />
-      ),
-    };
+    const { panel } = this.props;
+    const { currentDS, isAddingMixed } = this.state;
+    const { hasQueryHelp } = currentDS.meta;
 
     const queryInspector = {
       title: 'Query Inspector',
@@ -265,10 +331,31 @@ export class QueriesTab extends PureComponent<Props, State> {
     };
 
     return (
-      <EditorTabBody heading="Queries" main={dsInformation} toolbarItems={[options, queryInspector, dsHelp]}>
-        <>
-          <div ref={element => (this.element = element)} style={{ width: '100%' }} />
-        </>
+      <EditorTabBody
+        heading="Queries"
+        renderToolbar={this.renderToolbar}
+        toolbarItems={[options, queryInspector, dsHelp]}
+      >
+        <div className="query-editor-rows gf-form-group">
+          <div ref={element => (this.element = element)} />
+
+          <div className="gf-form-query">
+            <div className="gf-form gf-form-query-letter-cell">
+              <label className="gf-form-label">
+                <span className="gf-form-query-letter-cell-carret muted">
+                  <i className="fa fa-caret-down" />
+                </span>
+                <span className="gf-form-query-letter-cell-letter">{panel.getNextQueryLetter()}</span>
+              </label>
+              {!isAddingMixed && (
+                <button className="btn btn-secondary gf-form-btn" onClick={this.onAddQueryClick}>
+                  Add Query
+                </button>
+              )}
+              {isAddingMixed && this.renderMixedPicker()}
+            </div>
+          </div>
+        </div>
       </EditorTabBody>
     );
   }

+ 92 - 22
public/app/features/dashboard/dashgrid/VisualizationTab.tsx

@@ -7,6 +7,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
 // Components
 import { EditorTabBody } from './EditorTabBody';
 import { VizTypePicker } from './VizTypePicker';
+import { FadeIn } from 'app/core/components/Animations/FadeIn';
 
 // Types
 import { PanelModel } from '../panel_model';
@@ -21,9 +22,24 @@ interface Props {
   onTypeChanged: (newType: PanelPlugin) => void;
 }
 
-export class VisualizationTab extends PureComponent<Props> {
+interface State {
+  isVizPickerOpen: boolean;
+  searchQuery: string;
+}
+
+export class VisualizationTab extends PureComponent<Props, State> {
   element: HTMLElement;
   angularOptions: AngularComponent;
+  searchInput: HTMLElement;
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isVizPickerOpen: false,
+      searchQuery: '',
+    };
+  }
 
   getPanelDefaultOptions = () => {
     const { panel, plugin } = this.props;
@@ -87,10 +103,11 @@ export class VisualizationTab extends PureComponent<Props> {
 
     let template = '';
     for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
-      template += `
-      <div class="form-section" ng-cloak>
-        <div class="form-section__header">{{ctrl.editorTabs[${i}].title}}</div>
-        <div class="form-section__body">
+      template +=
+        `
+      <div class="form-section" ng-cloak>` +
+        (i > 0 ? `<div class="form-section__header">{{ctrl.editorTabs[${i}].title}}</div>` : '') +
+        `<div class="form-section__body">
           <panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
         </div>
       </div>
@@ -119,28 +136,81 @@ export class VisualizationTab extends PureComponent<Props> {
     this.forceUpdate();
   };
 
-  render() {
+  onOpenVizPicker = () => {
+    this.setState({ isVizPickerOpen: true });
+  };
+
+  onCloseVizPicker = () => {
+    this.setState({ isVizPickerOpen: false });
+  };
+
+  onSearchQueryChange = evt => {
+    const value = evt.target.value;
+    this.setState({
+      searchQuery: value,
+    });
+  };
+
+  renderToolbar = (): JSX.Element => {
     const { plugin } = this.props;
+    const { searchQuery } = this.state;
+
+    if (this.state.isVizPickerOpen) {
+      return (
+        <>
+          <label className="gf-form--has-input-icon">
+            <input
+              type="text"
+              className="gf-form-input width-13"
+              placeholder=""
+              onChange={this.onSearchQueryChange}
+              value={searchQuery}
+              ref={elem => elem && elem.focus()}
+            />
+            <i className="gf-form-input-icon fa fa-search" />
+          </label>
+          <div className="flex-grow" />
+          <button className="btn btn-link" onClick={this.onCloseVizPicker}>
+            <i className="fa fa-chevron-up" />
+          </button>
+        </>
+      );
+    } else {
+      return (
+        <div className="toolbar__main" onClick={this.onOpenVizPicker}>
+          <img className="toolbar__main-image" src={plugin.info.logos.small} />
+          <div className="toolbar__main-name">{plugin.name}</div>
+          <i className="fa fa-caret-down" />
+        </div>
+      );
+    }
+  };
 
-    const panelSelection = {
-      title: plugin.name,
-      imgSrc: plugin.info.logos.small,
-      render: () => {
-        // the needs to be scoped inside this closure
-        const { plugin, onTypeChanged } = this.props;
-        return <VizTypePicker current={plugin} onTypeChanged={onTypeChanged} />;
-      },
-    };
+  onTypeChanged = (plugin: PanelPlugin) => {
+    if (plugin.id === this.props.plugin.id) {
+      this.setState({ isVizPickerOpen: false });
+    } else {
+      this.props.onTypeChanged(plugin);
+    }
+  };
 
-    const panelHelp = {
-      title: '',
-      icon: 'fa fa-question',
-      render: () => <h2>Help</h2>,
-    };
+  render() {
+    const { plugin } = this.props;
+    const { isVizPickerOpen, searchQuery } = this.state;
 
     return (
-      <EditorTabBody heading="Visualization" main={panelSelection} toolbarItems={[panelHelp]}>
-        {this.renderPanelOptions()}
+      <EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar}>
+        <>
+          <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
+            <VizTypePicker
+              current={plugin}
+              onTypeChanged={this.onTypeChanged}
+              searchQuery={searchQuery}
+              onClose={this.onCloseVizPicker}
+            />
+          </FadeIn>
+          {this.renderPanelOptions()}
+        </>
       </EditorTabBody>
     );
   }

+ 10 - 70
public/app/features/dashboard/dashgrid/VizTypePicker.tsx

@@ -4,27 +4,20 @@ import _ from 'lodash';
 import config from 'app/core/config';
 import { PanelPlugin } from 'app/types/plugins';
 import VizTypePickerPlugin from './VizTypePickerPlugin';
-import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation';
 
 export interface Props {
   current: PanelPlugin;
   onTypeChanged: (newType: PanelPlugin) => void;
-}
-
-interface State {
   searchQuery: string;
+  onClose: () => void;
 }
 
-export class VizTypePicker extends PureComponent<Props, State> {
+export class VizTypePicker extends PureComponent<Props> {
   searchInput: HTMLElement;
   pluginList = this.getPanelPlugins('');
 
   constructor(props) {
     super(props);
-
-    this.state = {
-      searchQuery: '',
-    };
   }
 
   get maxSelectedIndex() {
@@ -32,12 +25,6 @@ export class VizTypePicker extends PureComponent<Props, State> {
     return filteredPluginList.length - 1;
   }
 
-  componentDidMount() {
-    setTimeout(() => {
-      this.searchInput.focus();
-    }, 300);
-  }
-
   getPanelPlugins(filter): PanelPlugin[] {
     const panels = _.chain(config.panels)
       .filter({ hideFromList: false })
@@ -48,27 +35,22 @@ export class VizTypePicker extends PureComponent<Props, State> {
     return _.sortBy(panels, 'sort');
   }
 
-  renderVizPlugin = (plugin: PanelPlugin, index: number, keyNavProps: KeyboardNavigationProps) => {
+  renderVizPlugin = (plugin: PanelPlugin, index: number) => {
     const { onTypeChanged } = this.props;
-    const { selected, onMouseEnter } = keyNavProps;
-    const isSelected = selected === index;
     const isCurrent = plugin.id === this.props.current.id;
+
     return (
       <VizTypePickerPlugin
         key={plugin.id}
-        isSelected={isSelected}
         isCurrent={isCurrent}
         plugin={plugin}
-        onMouseEnter={() => {
-          onMouseEnter(index);
-        }}
         onClick={() => onTypeChanged(plugin)}
       />
     );
   };
 
   getFilteredPluginList = (): PanelPlugin[] => {
-    const { searchQuery } = this.state;
+    const { searchQuery } = this.props;
     const regex = new RegExp(searchQuery, 'i');
     const pluginList = this.pluginList;
 
@@ -79,57 +61,15 @@ export class VizTypePicker extends PureComponent<Props, State> {
     return filtered;
   };
 
-  onSearchQueryChange = evt => {
-    const value = evt.target.value;
-    this.setState(prevState => ({
-      ...prevState,
-      searchQuery: value,
-    }));
-  };
-
-  renderFilters = ({ onKeyDown, selected }: KeyboardNavigationProps) => {
-    const { searchQuery } = this.state;
-    return (
-      <>
-        <label className="gf-form--has-input-icon">
-          <input
-            type="text"
-            className="gf-form-input width-13"
-            placeholder=""
-            ref={elem => (this.searchInput = elem)}
-            onChange={this.onSearchQueryChange}
-            value={searchQuery}
-            onKeyDown={evt => {
-              onKeyDown(evt, this.maxSelectedIndex, () => {
-                const { onTypeChanged } = this.props;
-                const vizType = this.getFilteredPluginList()[selected];
-                onTypeChanged(vizType);
-              });
-            }}
-          />
-          <i className="gf-form-input-icon fa fa-search" />
-        </label>
-      </>
-    );
-  };
-
   render() {
     const filteredPluginList = this.getFilteredPluginList();
 
     return (
-      <KeyboardNavigation
-        render={(keyNavProps: KeyboardNavigationProps) => (
-          <>
-            <div className="cta-form__bar">
-              {this.renderFilters(keyNavProps)}
-              <div className="gf-form--grow" />
-            </div>
-            <div className="viz-picker">
-              {filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index, keyNavProps))}
-            </div>
-          </>
-        )}
-      />
+      <div className="viz-picker">
+        <div className="viz-picker-list">
+          {filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index))}
+        </div>
+      </div>
     );
   }
 }

+ 4 - 7
public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx

@@ -1,32 +1,29 @@
-import React from 'react';
+import React from 'react';
 import classNames from 'classnames';
 import { PanelPlugin } from 'app/types/plugins';
 
 interface Props {
-  isSelected: boolean;
   isCurrent: boolean;
   plugin: PanelPlugin;
   onClick: () => void;
-  onMouseEnter: () => void;
 }
 
 const VizTypePickerPlugin = React.memo(
-  ({ isSelected, isCurrent, plugin, onClick, onMouseEnter }: Props) => {
+  ({ isCurrent, plugin, onClick }: Props) => {
     const cssClass = classNames({
       'viz-picker__item': true,
-      'viz-picker__item--selected': isSelected,
       'viz-picker__item--current': isCurrent,
     });
 
     return (
-      <div className={cssClass} onClick={onClick} title={plugin.name} onMouseEnter={onMouseEnter}>
+      <div className={cssClass} onClick={onClick} title={plugin.name}>
         <div className="viz-picker__item-name">{plugin.name}</div>
         <img className="viz-picker__item-img" src={plugin.info.logos.small} />
       </div>
     );
   },
   (prevProps, nextProps) => {
-    if (prevProps.isSelected === nextProps.isSelected && prevProps.isCurrent === nextProps.isCurrent) {
+    if (prevProps.isCurrent === nextProps.isCurrent) {
       return true;
     }
     return false;

+ 19 - 0
public/app/features/dashboard/panel_model.ts

@@ -1,6 +1,7 @@
 import { Emitter } from 'app/core/utils/emitter';
 import _ from 'lodash';
 import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
+import { DataQuery } from 'app/types';
 
 export interface GridPos {
   x: number;
@@ -237,6 +238,24 @@ export class PanelModel {
     this.restorePanelOptions(pluginId);
   }
 
+  addQuery(query?: Partial<DataQuery>) {
+    query = query || { refId: 'A' };
+    query.refId = this.getNextQueryLetter();
+    query.isNew = true;
+
+    this.targets.push(query);
+  }
+
+  getNextQueryLetter(): string {
+    const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+    return _.find(letters, refId => {
+      return _.every(this.targets, other => {
+        return other.refId !== refId;
+      });
+    });
+  }
+
   destroy() {
     this.events.emit('panel-teardown');
     this.events.removeAllListeners();

+ 9 - 167
public/app/features/panel/metrics_tab.ts

@@ -1,181 +1,24 @@
 // Libraries
 import _ from 'lodash';
-import Remarkable from 'remarkable';
 
 // Services & utils
 import coreModule from 'app/core/core_module';
-import config from 'app/core/config';
 import { Emitter } from 'app/core/utils/emitter';
 
 // Types
 import { DashboardModel } from '../dashboard/dashboard_model';
+import { PanelModel } from '../dashboard/panel_model';
+import { DataQuery } from 'app/types';
 
-export class MetricsTabCtrl {
-  dsName: string;
-  panel: any;
-  panelCtrl: any;
-  datasources: any[];
-  datasourceInstance: any;
-  nextRefId: string;
+export interface AngularQueryComponentScope {
+  panel: PanelModel;
   dashboard: DashboardModel;
-  panelDsValue: any;
-  addQueryDropdown: any;
-  queryTroubleshooterOpen: boolean;
-  helpOpen: boolean;
-  optionsOpen: boolean;
-  hasQueryHelp: boolean;
-  helpHtml: string;
-  queryOptions: any;
   events: Emitter;
-
-  /** @ngInject */
-  constructor($scope, private $sce, datasourceSrv, private backendSrv) {
-    this.panelCtrl = $scope.ctrl;
-    $scope.ctrl = this;
-
-    this.panel = this.panelCtrl.panel;
-    this.panel.datasource = this.panel.datasource || null;
-    this.panel.targets = this.panel.targets || [{}];
-
-    this.dashboard = this.panelCtrl.dashboard;
-    this.datasources = datasourceSrv.getMetricSources();
-    this.panelDsValue = this.panelCtrl.panel.datasource;
-
-    // added here as old query controller expects this on panelCtrl but
-    // they are getting MetricsTabCtrl instead
-    this.events = this.panel.events;
-
-    for (const ds of this.datasources) {
-      if (ds.value === this.panelDsValue) {
-        this.datasourceInstance = ds;
-      }
-    }
-
-    this.addQueryDropdown = { text: 'Add Query', value: null, fake: true };
-
-    // update next ref id
-    this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
-    this.updateDatasourceOptions();
-  }
-
-  updateDatasourceOptions() {
-    if (this.datasourceInstance) {
-      this.hasQueryHelp = this.datasourceInstance.meta.hasQueryHelp;
-      this.queryOptions = this.datasourceInstance.meta.queryOptions;
-    }
-  }
-
-  getOptions(includeBuiltin) {
-    return Promise.resolve(
-      this.datasources
-        .filter(value => {
-          return includeBuiltin || !value.meta.builtIn;
-        })
-        .map(ds => {
-          return { value: ds.value, text: ds.name, datasource: ds };
-        })
-    );
-  }
-
-  datasourceChanged(option) {
-    if (!option) {
-      return;
-    }
-
-    this.setDatasource(option.datasource);
-    this.updateDatasourceOptions();
-  }
-
-  setDatasource(datasource) {
-    // switching to mixed
-    if (datasource.meta.mixed) {
-      _.each(this.panel.targets, target => {
-        target.datasource = this.panel.datasource;
-        if (!target.datasource) {
-          target.datasource = config.defaultDatasource;
-        }
-      });
-    } else if (this.datasourceInstance) {
-      // if switching from mixed
-      if (this.datasourceInstance.meta.mixed) {
-        _.each(this.panel.targets, target => {
-          delete target.datasource;
-        });
-      } else if (this.datasourceInstance.meta.id !== datasource.meta.id) {
-        // we are changing data source type, clear queries
-        this.panel.targets = [{ refId: 'A' }];
-        this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
-      }
-    }
-
-    this.datasourceInstance = datasource;
-    this.panel.datasource = datasource.value;
-    this.panel.refresh();
-  }
-
-  addMixedQuery(option) {
-    if (!option) {
-      return;
-    }
-
-    this.panelCtrl.addQuery({
-      isNew: true,
-      datasource: option.datasource.name,
-    });
-    this.addQueryDropdown = { text: 'Add Query', value: null, fake: true };
-  }
-
-  toggleHelp() {
-    this.optionsOpen = false;
-    this.queryTroubleshooterOpen = false;
-    this.helpOpen = !this.helpOpen;
-
-    this.backendSrv.get(`/api/plugins/${this.datasourceInstance.meta.id}/markdown/query_help`).then(res => {
-      const md = new Remarkable();
-      this.helpHtml = this.$sce.trustAsHtml(md.render(res));
-    });
-  }
-
-  toggleOptions() {
-    this.helpOpen = false;
-    this.queryTroubleshooterOpen = false;
-    this.optionsOpen = !this.optionsOpen;
-  }
-
-  toggleQueryTroubleshooter() {
-    this.helpOpen = false;
-    this.optionsOpen = false;
-    this.queryTroubleshooterOpen = !this.queryTroubleshooterOpen;
-  }
-
-  addQuery(query?) {
-    query = query || {};
-    query.refId = this.dashboard.getNextQueryLetter(this.panel);
-    query.isNew = true;
-
-    this.panel.targets.push(query);
-    this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
-  }
-
-  refresh() {
-    this.panel.refresh();
-  }
-
-  render() {
-    this.panel.render();
-  }
-
-  removeQuery(target) {
-    const index = _.indexOf(this.panel.targets, target);
-    this.panel.targets.splice(index, 1);
-    this.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
-    this.panel.refresh();
-  }
-
-  moveQuery(target, direction) {
-    const index = _.indexOf(this.panel.targets, target);
-    _.move(this.panel.targets, index, index + direction);
-  }
+  refresh: () => void;
+  render: () => void;
+  removeQuery: (query: DataQuery) => void;
+  addQuery: (query?: DataQuery) => void;
+  moveQuery: (query: DataQuery, direction: number) => void;
 }
 
 /** @ngInject */
@@ -185,7 +28,6 @@ export function metricsTabDirective() {
     restrict: 'E',
     scope: true,
     templateUrl: 'public/app/features/panel/partials/metrics_tab.html',
-    controller: MetricsTabCtrl,
   };
 }
 

+ 17 - 20
public/app/features/panel/partials/metrics_tab.html

@@ -1,5 +1,3 @@
-
-<div class="query-editor-rows gf-form-group" ng-if="ctrl.datasourceInstance">
 	<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
 		<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
 			<plugin-component type="query-ctrl">
@@ -7,21 +5,20 @@
 		</rebuild-on-change>
 	</div>
 
-	<div class="gf-form-query">
-		<div class="gf-form gf-form-query-letter-cell">
-			<label class="gf-form-label">
-				<span class="gf-form-query-letter-cell-carret">
-					<i class="fa fa-caret-down"></i>
-				</span>
-				<span class="gf-form-query-letter-cell-letter">{{ctrl.nextRefId}}</span>
-			</label>
-			<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.datasourceInstance.meta.mixed">
-				Add Query
-			</button>
-			<div class="dropdown" ng-if="ctrl.datasourceInstance.meta.mixed">
-				<gf-form-dropdown model="ctrl.addQueryDropdown" get-options="ctrl.getOptions(false)" on-change="ctrl.addMixedQuery($option)">
-				</gf-form-dropdown>
-			</div>
-		</div>
-	</div>
-</div>
+	<!-- <div class="gf&#45;form&#45;query"> -->
+	<!-- 	<div class="gf&#45;form gf&#45;form&#45;query&#45;letter&#45;cell"> -->
+	<!-- 		<label class="gf&#45;form&#45;label"> -->
+	<!-- 			<span class="gf&#45;form&#45;query&#45;letter&#45;cell&#45;carret"> -->
+	<!-- 				<i class="fa fa&#45;caret&#45;down"></i> -->
+	<!-- 			</span> -->
+	<!-- 			<span class="gf&#45;form&#45;query&#45;letter&#45;cell&#45;letter">{{ctrl.nextRefId}}</span> -->
+	<!-- 		</label> -->
+	<!-- 		<button class="btn btn&#45;secondary gf&#45;form&#45;btn" ng&#45;click="ctrl.addQuery()" ng&#45;hide="ctrl.datasourceInstance.meta.mixed"> -->
+	<!-- 			Add Query -->
+	<!-- 		</button> -->
+	<!-- 		<div class="dropdown" ng&#45;if="ctrl.datasourceInstance.meta.mixed"> -->
+	<!-- 			<gf&#45;form&#45;dropdown model="ctrl.addQueryDropdown" get&#45;options="ctrl.getOptions(false)" on&#45;change="ctrl.addMixedQuery($option)"> -->
+	<!-- 			</gf&#45;form&#45;dropdown> -->
+	<!-- 		</div> -->
+	<!-- 	</div> -->
+	<!-- </div> -->

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

@@ -20,7 +20,7 @@ export class QueryRowCtrl {
     this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions;
 
     if (!this.target.refId) {
-      this.target.refId = this.panelCtrl.dashboard.getNextQueryLetter(this.panel);
+      this.target.refId = this.panel.getNextQueryLetter();
     }
 
     this.toggleCollapse(true);

+ 2 - 2
public/app/plugins/datasource/prometheus/partials/query.editor.html

@@ -8,7 +8,7 @@
   </div>
 
   <div class="gf-form-inline">
-    <div class="gf-form max-width-26">
+    <div class="gf-form">
       <label class="gf-form-label width-8">Legend format</label>
       <input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
         data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
@@ -58,4 +58,4 @@
       <div class="gf-form-label gf-form-label--grow"></div>
     </div>
   </div>
-</query-editor-row>
+</query-editor-row>

+ 0 - 2
public/sass/_grafana.scss

@@ -96,8 +96,6 @@
 @import 'components/empty_list_cta';
 @import 'components/popper';
 @import 'components/form_select_box';
-@import 'components/user-picker';
-@import 'components/description-picker';
 @import 'components/panel_editor';
 @import 'components/toolbar';
 @import 'components/delete_button';

+ 4 - 3
public/sass/_variables.dark.scss

@@ -271,7 +271,7 @@ $menu-dropdown-shadow: 5px 5px 20px -5px $black;
 $tab-border-color: $dark-4;
 
 // Toolbar
-$toolbar-bg: $black;
+$toolbar-bg: $input-black;
 
 // Pagination
 // -------------------------
@@ -375,13 +375,14 @@ $checkbox-color: $dark-1;
 //Panel Edit
 // -------------------------
 $panel-editor-shadow: 0 0 20px black;
+$panel-editor-border: 1px solid $dark-3;
 $panel-editor-side-menu-shadow: drop-shadow(0 0 10px $black);
-$panel-editor-toolbar-view-bg: $black;
+$panel-editor-toolbar-view-bg: $input-black;
 $panel-editor-viz-item-shadow: 0 0 8px $dark-5;
 $panel-editor-viz-item-border: 1px solid $dark-5;
 $panel-editor-viz-item-shadow-hover: 0 0 4px $blue;
 $panel-editor-viz-item-border-hover: 1px solid $blue;
-$panel-editor-viz-item-bg: $black;
+$panel-editor-viz-item-bg: $input-black;
 $panel-editor-tabs-line-color: #e3e3e3;
 $panel-editor-viz-item-bg-hover: darken($blue, 47%);
 $panel-editor-viz-item-bg-hover-active: darken($orange, 45%);

+ 1 - 0
public/sass/_variables.light.scss

@@ -384,6 +384,7 @@ $checkbox-color: $gray-7;
 //Panel Edit
 // -------------------------
 $panel-editor-shadow: 2px 2px 8px $gray-3;
+$panel-editor-border: 1px solid $dark-4;
 $panel-editor-side-menu-shadow: drop-shadow(0 0 2px $gray-3);
 $panel-editor-toolbar-view-bg: $white;
 $panel-editor-viz-item-shadow: 0 0 4px $gray-3;

+ 1 - 0
public/sass/components/_buttons.scss

@@ -78,6 +78,7 @@
 
 .btn-link {
   color: $btn-link-color;
+  background: transparent;
 }
 
 // Set the backgrounds

+ 0 - 11
public/sass/components/_description-picker.scss

@@ -1,11 +0,0 @@
-.description-picker-option__button {
-  position: relative;
-  text-align: left;
-  width: 100%;
-  display: block;
-  border-radius: 0;
-  white-space: normal;
-  i.fa-check {
-    padding-left: 2px;
-  }
-}

+ 42 - 2
public/sass/components/_form_select_box.scss

@@ -47,12 +47,17 @@ $select-input-bg-disabled: $input-bg-disabled;
 
 .gf-form-select-box__input {
   padding-left: 5px;
+  input {
+    line-height: inherit;
+  }
 }
 
 .gf-form-select-box__menu {
-  background: $dropdownBackground;
+  background: $input-bg;
+  box-shadow: $menu-dropdown-shadow;
   position: absolute;
   z-index: 2;
+  min-width: 100%;
 }
 
 .gf-form-select-box__menu-list {
@@ -64,16 +69,20 @@ $select-input-bg-disabled: $input-bg-disabled;
   width: 100%;
 }
 
+/* .gf-form-select-box__single-value { */
+/* } */
+
 .gf-form-select-box__multi-value {
   display: inline;
 }
 
 .gf-form-select-box__option {
   border-left: 2px solid transparent;
+  white-space: nowrap;
 
   &.gf-form-select-box__option--is-focused {
     color: $dropdownLinkColorHover;
-    background-color: $dropdownLinkBackgroundHover;
+    background: $menu-dropdown-hover-bg;
     @include left-brand-border-gradient();
   }
 
@@ -119,10 +128,12 @@ $select-input-bg-disabled: $input-bg-disabled;
     border-width: 0 5px 5px;
   }
 }
+
 .gf-form-input--form-dropdown {
   padding: 0;
   border: 0;
   overflow: visible;
+  position: relative;
 }
 
 .gf-form--has-input-icon {
@@ -130,3 +141,32 @@ $select-input-bg-disabled: $input-bg-disabled;
     padding-left: 30px;
   }
 }
+
+.gf-form-select-box__desc-option {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  justify-items: center;
+  cursor: pointer;
+  padding: 7px 10px;
+  width: 100%;
+}
+
+.gf-form-select-box__desc-option__body {
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+  padding-right: 10px;
+  font-weight: 500;
+}
+
+.gf-form-select-box__desc-option__desc {
+  font-weight: normal;
+  font-size: $font-size-sm;
+  color: $text-muted;
+}
+
+.gf-form-select-box__desc-option__img {
+  width: 16px;
+  margin-right: 10px;
+}

+ 0 - 1
public/sass/components/_gf-form.scss

@@ -110,7 +110,6 @@ $input-border: 1px solid $input-border-color;
 
   &--grow {
     flex-grow: 1;
-    min-height: 2.6rem;
   }
 
   &--error {

+ 5 - 3
public/sass/components/_json_explorer.scss

@@ -21,10 +21,10 @@
         display: none;
       }
       &.json-formatter-object::after {
-        content: "No properties";
+        content: 'No properties';
       }
       &.json-formatter-array::after {
-        content: "[]";
+        content: '[]';
       }
     }
   }
@@ -33,7 +33,9 @@
     color: $json-explorer-string-color;
     white-space: normal;
     word-wrap: break-word;
+    word-break: break-all;
   }
+
   .json-formatter-number {
     color: $json-explorer-number-color;
   }
@@ -87,7 +89,7 @@
     &::after {
       display: inline-block;
       transition: transform $json-explorer-rotate-time ease-in;
-      content: "►";
+      content: '►';
     }
   }
 

+ 30 - 5
public/sass/components/_panel_editor.scss

@@ -34,7 +34,6 @@
   flex-grow: 1;
   background: $page-bg;
   margin: 0 20px 0 84px;
-  border-left: 2px solid $orange;
   border-radius: 3px;
   box-shadow: $panel-editor-shadow;
 }
@@ -133,14 +132,19 @@
 }
 
 .viz-picker {
+  margin-top: -40px;
+  padding: 20px;
+  position: relative;
+}
+
+.viz-picker-list {
   display: flex;
   flex-wrap: wrap;
-  margin-bottom: 13px;
 }
 
 .viz-picker__item {
-  background: $panel-editor-viz-item-bg;
-  border: $panel-editor-viz-item-border;
+  background: $panel-bg;
+  border: $panel-border;
   border-radius: 3px;
   height: 100px;
   width: 150px;
@@ -162,7 +166,7 @@
     border: 1px solid $orange;
   }
 
-  &--selected {
+  &:hover {
     box-shadow: $panel-editor-viz-item-shadow-hover;
     background: $panel-editor-viz-item-bg-hover;
     border: $panel-editor-viz-item-border-hover;
@@ -273,6 +277,20 @@
   }
 }
 
+.ds-picker {
+  position: relative;
+  min-width: 200px;
+}
+
+.ds-picker-menu {
+  min-width: 400px;
+  max-width: 500px;
+  position: absolute;
+  background: $panel-editor-toolbar-view-bg;
+  padding: 5px;
+  overflow: auto;
+}
+
 .ds-picker-list__name {
   text-overflow: ellipsis;
   overflow: hidden;
@@ -306,6 +324,13 @@
   margin-bottom: 20px;
   background: $input-label-bg;
   border-radius: 3px;
+  position: relative;
+
+  .btn {
+    position: absolute;
+    right: 0;
+    top: 2px;
+  }
 }
 
 .form-section__body {

+ 0 - 12
public/sass/components/_user-picker.scss

@@ -1,12 +0,0 @@
-.user-picker-option__button {
-  position: relative;
-  text-align: left;
-  width: 100%;
-  display: block;
-  border-radius: 0;
-}
-.user-picker-option__avatar {
-  width: 20px;
-  display: inline-block;
-  margin-right: 10px;
-}

+ 4 - 0
public/sass/utils/_utils.scss

@@ -83,6 +83,10 @@ button.close {
   position: absolute;
 }
 
+.flex-grow {
+  flex-grow: 1;
+}
+
 .center-vh {
   display: flex;
   align-items: center;