Ver Fonte

Merge pull request #14930 from grafana/react-query-editor

React query editor (part1)
Torkel Ödegaard há 7 anos atrás
pai
commit
dfe1b20f3d

+ 33 - 0
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -1,4 +1,5 @@
 import React, { PureComponent } from 'react';
+import _ from 'lodash';
 import Scrollbars from 'react-custom-scrollbars';
 
 interface Props {
@@ -8,6 +9,8 @@ interface Props {
   autoHideDuration?: number;
   autoMaxHeight?: string;
   hideTracksWhenNotNeeded?: boolean;
+  scrollTop?: number;
+  setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
   autoHeightMin?: number | string;
 }
 
@@ -22,14 +25,44 @@ export class CustomScrollbar extends PureComponent<Props> {
     autoHideDuration: 200,
     autoMaxHeight: '100%',
     hideTracksWhenNotNeeded: false,
+    scrollTop: 0,
+    setScrollTop: () => {},
     autoHeightMin: '0'
   };
 
+  private ref: React.RefObject<Scrollbars>;
+
+  constructor(props: Props) {
+    super(props);
+    this.ref = React.createRef<Scrollbars>();
+  }
+
+  updateScroll() {
+    const ref = this.ref.current;
+
+    if (ref && !_.isNil(this.props.scrollTop)) {
+      if (this.props.scrollTop > 10000) {
+        ref.scrollToBottom();
+      } else {
+        ref.scrollTop(this.props.scrollTop);
+      }
+   }
+  }
+
+  componentDidMount() {
+    this.updateScroll();
+  }
+
+  componentDidUpdate() {
+    this.updateScroll();
+  }
+
   render() {
     const { customClassName, children, autoMaxHeight } = this.props;
 
     return (
       <Scrollbars
+        ref={this.ref}
         className={customClassName}
         autoHeight={true}
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.

+ 4 - 2
public/app/features/dashboard/panel_editor/EditorTabBody.tsx

@@ -10,6 +10,8 @@ interface Props {
   heading: string;
   renderToolbar?: () => JSX.Element;
   toolbarItems?: EditorToolbarView[];
+  scrollTop?: number;
+  setScrollTop?: (value: React.MouseEvent<HTMLElement>) => void;
 }
 
 export interface EditorToolbarView {
@@ -103,7 +105,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
   }
 
   render() {
-    const { children, renderToolbar, heading, toolbarItems } = this.props;
+    const { children, renderToolbar, heading, toolbarItems, scrollTop, setScrollTop } = this.props;
     const { openView, fadeIn, isOpen } = this.state;
 
     return (
@@ -119,7 +121,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
           )}
         </div>
         <div className="panel-editor__scroll">
-          <CustomScrollbar autoHide={false}>
+          <CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
             <div className="panel-editor__content">
               <FadeIn in={isOpen} duration={200} unmountOnExit={true}>
                 {openView && this.renderOpenView(openView)}

+ 54 - 85
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -3,18 +3,16 @@ import React, { PureComponent } from 'react';
 import _ from 'lodash';
 
 // Components
-import 'app/features/panel/metrics_tab';
 import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { QueryInspector } from './QueryInspector';
 import { QueryOptions } from './QueryOptions';
-import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab';
 import { PanelOptionsGroup } from '@grafana/ui';
+import { QueryEditorRow } from './QueryEditorRow';
 
 // Services
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
-import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
 import config from 'app/core/config';
 
 // Types
@@ -34,66 +32,27 @@ interface State {
   isLoadingHelp: boolean;
   isPickerOpen: boolean;
   isAddingMixed: boolean;
+  scrollTop: number;
 }
 
 export class QueriesTab extends PureComponent<Props, State> {
-  element: HTMLElement;
-  component: AngularComponent;
   datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
   backendSrv: BackendSrv = getBackendSrv();
 
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isLoadingHelp: false,
-      currentDS: this.findCurrentDataSource(),
-      helpContent: null,
-      isPickerOpen: false,
-      isAddingMixed: false,
-    };
-  }
+  state: State = {
+    isLoadingHelp: false,
+    currentDS: this.findCurrentDataSource(),
+    helpContent: null,
+    isPickerOpen: false,
+    isAddingMixed: false,
+    scrollTop: 0,
+  };
 
   findCurrentDataSource(): DataSourceSelectItem {
     const { panel } = this.props;
     return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0];
   }
 
-  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,
-    };
-  }
-
-  componentDidMount() {
-    if (!this.element) {
-      return;
-    }
-
-    const loader = getAngularLoader();
-    const template = '<metrics-tab />';
-    const scopeProps = {
-      ctrl: this.getAngularQueryComponentScope(),
-    };
-
-    this.component = loader.load(this.element, scopeProps, template);
-  }
-
-  componentWillUnmount() {
-    if (this.component) {
-      this.component.destroy();
-    }
-  }
-
   onChangeDataSource = datasource => {
     const { panel } = this.props;
     const { currentDS } = this.state;
@@ -137,7 +96,7 @@ export class QueriesTab extends PureComponent<Props, State> {
 
   onAddQuery = (query?: Partial<DataQuery>) => {
     this.props.panel.addQuery(query);
-    this.forceUpdate();
+    this.setState({ scrollTop: this.state.scrollTop + 100000 });
   };
 
   onAddQueryClick = () => {
@@ -146,9 +105,7 @@ export class QueriesTab extends PureComponent<Props, State> {
       return;
     }
 
-    this.props.panel.addQuery();
-    this.component.digest();
-    this.forceUpdate();
+    this.onAddQuery();
   };
 
   onRemoveQuery = (query: DataQuery) => {
@@ -171,9 +128,21 @@ export class QueriesTab extends PureComponent<Props, State> {
   };
 
   renderToolbar = () => {
-    const { currentDS } = this.state;
+    const { currentDS, isAddingMixed } = this.state;
 
-    return <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />;
+    return (
+      <>
+        <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
+        <div className="m-l-2">
+          {!isAddingMixed && (
+            <button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
+              Add Query
+            </button>
+          )}
+          {isAddingMixed && this.renderMixedPicker()}
+        </div>
+      </>
+    );
   };
 
   renderMixedPicker = () => {
@@ -190,17 +159,21 @@ export class QueriesTab extends PureComponent<Props, State> {
 
   onAddMixedQuery = datasource => {
     this.onAddQuery({ datasource: datasource.name });
-    this.component.digest();
-    this.setState({ isAddingMixed: false });
+    this.setState({ isAddingMixed: false, scrollTop: this.state.scrollTop + 10000 });
   };
 
   onMixedPickerBlur = () => {
     this.setState({ isAddingMixed: false });
   };
 
+  setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
+    const target = event.target as HTMLElement;
+    this.setState({ scrollTop: target.scrollTop });
+  };
+
   render() {
     const { panel } = this.props;
-    const { currentDS, isAddingMixed } = this.state;
+    const { currentDS, scrollTop } = this.state;
 
     const queryInspector: EditorToolbarView = {
       title: 'Query Inspector',
@@ -214,32 +187,28 @@ export class QueriesTab extends PureComponent<Props, State> {
     };
 
     return (
-      <EditorTabBody heading="Queries" renderToolbar={this.renderToolbar} toolbarItems={[queryInspector, dsHelp]}>
+      <EditorTabBody
+        heading="Queries to"
+        renderToolbar={this.renderToolbar}
+        toolbarItems={[queryInspector, dsHelp]}
+        setScrollTop={this.setScrollTop}
+        scrollTop={scrollTop}
+      >
         <>
-          <PanelOptionsGroup>
-            <div className="query-editor-rows">
-              <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>
-                </div>
-                <div className="gf-form">
-                  {!isAddingMixed && (
-                    <button className="btn btn-secondary gf-form-btn" onClick={this.onAddQueryClick}>
-                      Add Query
-                    </button>
-                  )}
-                  {isAddingMixed && this.renderMixedPicker()}
-                </div>
-              </div>
-            </div>
-          </PanelOptionsGroup>
+          <div className="query-editor-rows">
+            {panel.targets.map((query, index) => (
+              <QueryEditorRow
+                datasourceName={query.datasource || panel.datasource}
+                key={query.refId}
+                panel={panel}
+                query={query}
+                onRemoveQuery={this.onRemoveQuery}
+                onAddQuery={this.onAddQuery}
+                onMoveQuery={this.onMoveQuery}
+                inMixedMode={currentDS.meta.mixed}
+              />
+            ))}
+          </div>
           <PanelOptionsGroup>
             <QueryOptions panel={panel} datasource={currentDS} />
           </PanelOptionsGroup>

+ 237 - 0
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -0,0 +1,237 @@
+// Libraries
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import _ from 'lodash';
+
+// Utils & Services
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
+import { Emitter } from 'app/core/utils/emitter';
+
+// Types
+import { PanelModel } from '../panel_model';
+import { DataQuery, DataSourceApi } from 'app/types/series';
+
+interface Props {
+  panel: PanelModel;
+  query: DataQuery;
+  onAddQuery: (query?: DataQuery) => void;
+  onRemoveQuery: (query: DataQuery) => void;
+  onMoveQuery: (query: DataQuery, direction: number) => void;
+  datasourceName: string | null;
+  inMixedMode: boolean;
+}
+
+interface State {
+  datasource: DataSourceApi | null;
+  isCollapsed: boolean;
+  angularScope: AngularQueryComponentScope | null;
+}
+
+export class QueryEditorRow extends PureComponent<Props, State> {
+  element: HTMLElement | null = null;
+  angularQueryEditor: AngularComponent | null = null;
+
+  state: State = {
+    datasource: null,
+    isCollapsed: false,
+    angularScope: null,
+  };
+
+  componentDidMount() {
+    this.loadDatasource();
+  }
+
+  getAngularQueryComponentScope(): AngularQueryComponentScope {
+    const { panel, query } = this.props;
+    const { datasource } = this.state;
+
+    return {
+      datasource: datasource,
+      target: query,
+      panel: panel,
+      refresh: () => panel.refresh(),
+      render: () => panel.render,
+      events: panel.events,
+    };
+  }
+
+  async loadDatasource() {
+    const { query, panel } = this.props;
+    const dataSourceSrv = getDatasourceSrv();
+    const datasource = await dataSourceSrv.get(query.datasource || panel.datasource);
+
+    this.setState({ datasource });
+  }
+
+  componentDidUpdate() {
+    const { datasource } = this.state;
+
+    // check if we need to load another datasource
+    if (datasource && datasource.name !== this.props.datasourceName) {
+      if (this.angularQueryEditor) {
+        this.angularQueryEditor.destroy();
+        this.angularQueryEditor = null;
+      }
+      this.loadDatasource();
+      return;
+    }
+
+    if (!this.element || this.angularQueryEditor) {
+      return;
+    }
+
+    const loader = getAngularLoader();
+    const template = '<plugin-component type="query-ctrl" />';
+    const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
+
+    this.angularQueryEditor = loader.load(this.element, scopeProps, template);
+
+    // give angular time to compile
+    setTimeout(() => {
+      this.setState({ angularScope: scopeProps.ctrl });
+    }, 10);
+  }
+
+  componentWillUnmount() {
+    if (this.angularQueryEditor) {
+      this.angularQueryEditor.destroy();
+    }
+  }
+
+  onToggleCollapse = () => {
+    this.setState({ isCollapsed: !this.state.isCollapsed });
+  };
+
+  renderPluginEditor() {
+    const { datasource } = this.state;
+
+    if (datasource.pluginExports.QueryCtrl) {
+      return <div ref={element => (this.element = element)} />;
+    }
+
+    if (datasource.pluginExports.QueryEditor) {
+      const QueryEditor = datasource.pluginExports.QueryEditor;
+      return <QueryEditor />;
+    }
+
+    return <div>Data source plugin does not export any Query Editor component</div>;
+  }
+
+  onToggleEditMode = () => {
+    const { angularScope } = this.state;
+
+    if (angularScope && angularScope.toggleEditorMode) {
+      angularScope.toggleEditorMode();
+      this.angularQueryEditor.digest();
+    }
+
+    if (this.state.isCollapsed) {
+      this.setState({ isCollapsed: false });
+    }
+  };
+
+  get hasTextEditMode() {
+    const { angularScope } = this.state;
+    return angularScope && angularScope.toggleEditorMode;
+  }
+
+  onRemoveQuery = () => {
+    this.props.onRemoveQuery(this.props.query);
+  };
+
+  onCopyQuery = () => {
+    const copy = _.cloneDeep(this.props.query);
+    this.props.onAddQuery(copy);
+  };
+
+  onDisableQuery = () => {
+    this.props.query.hide = !this.props.query.hide;
+    this.forceUpdate();
+  };
+
+  renderCollapsedText(): string | null {
+    const { angularScope } = this.state;
+
+    if (angularScope && angularScope.getCollapsedText) {
+      return angularScope.getCollapsedText();
+    }
+
+    return null;
+  }
+
+  render() {
+    const { query, datasourceName, inMixedMode } = this.props;
+    const { datasource, isCollapsed } = this.state;
+    const isDisabled = query.hide;
+
+    const bodyClasses = classNames('query-editor-row__body gf-form-query', {
+      'query-editor-row__body--collapsed': isCollapsed,
+    });
+
+    const rowClasses = classNames('query-editor-row', {
+      'query-editor-row--disabled': isDisabled,
+      'gf-form-disabled': isDisabled,
+    });
+
+    if (!datasource) {
+      return null;
+    }
+
+    return (
+      <div className={rowClasses}>
+        <div className="query-editor-row__header">
+          <div className="query-editor-row__ref-id" onClick={this.onToggleCollapse}>
+            {isCollapsed && <i className="fa fa-caret-right" />}
+            {!isCollapsed && <i className="fa fa-caret-down" />}
+            <span>{query.refId}</span>
+            {inMixedMode && <em className="query-editor-row__context-info"> ({datasourceName})</em>}
+            {isDisabled && <em className="query-editor-row__context-info"> Disabled</em>}
+          </div>
+          <div className="query-editor-row__collapsed-text">
+            {isCollapsed && <div>{this.renderCollapsedText()}</div>}
+          </div>
+          <div className="query-editor-row__actions">
+            {this.hasTextEditMode && (
+              <button
+                className="query-editor-row__action"
+                onClick={this.onToggleEditMode}
+                title="Toggle text edit mode"
+              >
+                <i className="fa fa-fw fa-pencil" />
+              </button>
+            )}
+            <button className="query-editor-row__action" onClick={() => this.props.onMoveQuery(query, 1)}>
+              <i className="fa fa-fw fa-arrow-down" />
+            </button>
+            <button className="query-editor-row__action" onClick={() => this.props.onMoveQuery(query, -1)}>
+              <i className="fa fa-fw fa-arrow-up" />
+            </button>
+            <button className="query-editor-row__action" onClick={this.onCopyQuery} title="Duplicate query">
+              <i className="fa fa-fw fa-copy" />
+            </button>
+            <button className="query-editor-row__action" onClick={this.onDisableQuery} title="Disable/enable query">
+              {isDisabled && <i className="fa fa-fw fa-eye-slash" />}
+              {!isDisabled && <i className="fa fa-fw fa-eye" />}
+            </button>
+            <button className="query-editor-row__action" onClick={this.onRemoveQuery} title="Remove query">
+              <i className="fa fa-fw fa-trash" />
+            </button>
+          </div>
+        </div>
+        <div className={bodyClasses}>{this.renderPluginEditor()}</div>
+      </div>
+    );
+  }
+}
+
+export interface AngularQueryComponentScope {
+  target: DataQuery;
+  panel: PanelModel;
+  events: Emitter;
+  refresh: () => void;
+  render: () => void;
+  datasource: DataSourceApi;
+  toggleEditorMode?: () => void;
+  getCollapsedText?: () => string;
+}

+ 11 - 3
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -26,6 +26,7 @@ interface Props {
 interface State {
   isVizPickerOpen: boolean;
   searchQuery: string;
+  scrollTop: number;
 }
 
 export class VisualizationTab extends PureComponent<Props, State> {
@@ -39,6 +40,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
     this.state = {
       isVizPickerOpen: false,
       searchQuery: '',
+      scrollTop: 0,
     };
   }
 
@@ -143,7 +145,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
   };
 
   onOpenVizPicker = () => {
-    this.setState({ isVizPickerOpen: true });
+    this.setState({ isVizPickerOpen: true, scrollTop: 0 });
   };
 
   onCloseVizPicker = () => {
@@ -201,9 +203,14 @@ export class VisualizationTab extends PureComponent<Props, State> {
 
   renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />;
 
+  setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
+    const target = event.target as HTMLElement;
+    this.setState({ scrollTop: target.scrollTop });
+  };
+
   render() {
     const { plugin } = this.props;
-    const { isVizPickerOpen, searchQuery } = this.state;
+    const { isVizPickerOpen, searchQuery, scrollTop } = this.state;
 
     const pluginHelp: EditorToolbarView = {
       heading: 'Help',
@@ -212,7 +219,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
     };
 
     return (
-      <EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}>
+      <EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}
+        scrollTop={scrollTop} setScrollTop={this.setScrollTop}>
         <>
           <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
             <VizTypePicker

+ 0 - 31
public/app/features/panel/metrics_tab.ts

@@ -1,31 +0,0 @@
-// Services & utils
-import coreModule from 'app/core/core_module';
-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 interface AngularQueryComponentScope {
-  panel: PanelModel;
-  dashboard: DashboardModel;
-  events: Emitter;
-  refresh: () => void;
-  render: () => void;
-  removeQuery: (query: DataQuery) => void;
-  addQuery: (query?: DataQuery) => void;
-  moveQuery: (query: DataQuery, direction: number) => void;
-}
-
-/** @ngInject */
-export function metricsTabDirective() {
-  'use strict';
-  return {
-    restrict: 'E',
-    scope: true,
-    templateUrl: 'public/app/features/panel/partials/metrics_tab.html',
-  };
-}
-
-coreModule.directive('metricsTab', metricsTabDirective);

+ 0 - 24
public/app/features/panel/partials/metrics_tab.html

@@ -1,24 +0,0 @@
-	<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">
-			</plugin-component>
-		</rebuild-on-change>
-	</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 - 43
public/app/features/panel/partials/query_editor_row.html

@@ -1,44 +1,2 @@
-<div class="gf-form-query">
-  <div ng-if="!ctrl.hideEditorRowActions" class="gf-form gf-form-query-letter-cell">
-    <label class="gf-form-label">
-      <a class="pointer" tabindex="1" ng-click="ctrl.toggleCollapse()">
-        <span ng-class="{muted: !ctrl.canCollapse}" class="gf-form-query-letter-cell-carret">
-          <i class="fa fa-caret-down" ng-hide="ctrl.collapsed"></i>
-          <i class="fa fa-caret-right" ng-show="ctrl.collapsed"></i>
-        </span>
-        <span class="gf-form-query-letter-cell-letter">{{ ctrl.target.refId }}</span>
-        <em class="gf-form-query-letter-cell-ds" ng-show="ctrl.target.datasource">({{ ctrl.target.datasource }})</em>
-      </a>
-    </label>
-  </div>
+<div ng-transclude class="gf-form-query-content"></div>
 
-  <div class="gf-form-query-content gf-form-query-content--collapsed" ng-if="ctrl.collapsed">
-    <div class="gf-form">
-      <label class="gf-form-label pointer gf-form-label--grow" ng-click="ctrl.toggleCollapse()">
-        {{ ctrl.collapsedText }}
-      </label>
-    </div>
-  </div>
-
-  <div ng-transclude class="gf-form-query-content" ng-if="!ctrl.collapsed"></div>
-
-  <div ng-if="!ctrl.hideEditorRowActions" class="gf-form">
-    <label class="gf-form-label dropdown">
-      <a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1"> <i class="fa fa-bars"></i> </a>
-      <ul class="dropdown-menu pull-right" role="menu">
-        <li role="menuitem" ng-if="ctrl.hasTextEditMode">
-          <a tabindex="1" ng-click="ctrl.toggleEditorMode()">Toggle Edit Mode</a>
-        </li>
-        <li role="menuitem"><a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a></li>
-        <li role="menuitem"><a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a></li>
-        <li role="menuitem"><a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a></li>
-      </ul>
-    </label>
-    <label class="gf-form-label">
-      <a ng-click="ctrl.toggleHideQuery()" role="menuitem"> <i class="fa fa-eye"></i> </a>
-    </label>
-    <label class="gf-form-label">
-      <a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(ctrl.target)"> <i class="fa fa-trash"></i> </a>
-    </label>
-  </div>
-</div>

+ 7 - 70
public/app/features/panel/query_editor_row.ts

@@ -3,89 +3,26 @@ import angular from 'angular';
 const module = angular.module('grafana.directives');
 
 export class QueryRowCtrl {
-  collapsedText: string;
-  canCollapse: boolean;
-  getCollapsedText: any;
   target: any;
   queryCtrl: any;
   panelCtrl: any;
   panel: any;
-  collapsed: any;
-  hideEditorRowActions: boolean;
+  hasTextEditMode: boolean;
 
   constructor() {
     this.panelCtrl = this.queryCtrl.panelCtrl;
     this.target = this.queryCtrl.target;
     this.panel = this.panelCtrl.panel;
-    this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions;
 
-    if (!this.target.refId) {
-      this.target.refId = this.panel.getNextQueryLetter();
+    if (this.hasTextEditMode) {
+      // expose this function to react parent component
+      this.panelCtrl.toggleEditorMode = this.queryCtrl.toggleEditorMode.bind(this.queryCtrl);
     }
 
-    this.toggleCollapse(true);
-    if (this.target.isNew) {
-      delete this.target.isNew;
-      this.toggleCollapse(false);
+    if (this.queryCtrl.getCollapsedText) {
+      // expose this function to react parent component
+      this.panelCtrl.getCollapsedText = this.queryCtrl.getCollapsedText.bind(this.queryCtrl);
     }
-
-    if (this.panel.targets.length < 4) {
-      this.collapsed = false;
-    }
-  }
-
-  toggleHideQuery() {
-    this.target.hide = !this.target.hide;
-    this.panelCtrl.refresh();
-  }
-
-  toggleCollapse(init) {
-    if (!this.canCollapse) {
-      return;
-    }
-
-    if (!this.panelCtrl.__collapsedQueryCache) {
-      this.panelCtrl.__collapsedQueryCache = {};
-    }
-
-    if (init) {
-      this.collapsed = this.panelCtrl.__collapsedQueryCache[this.target.refId] !== false;
-    } else {
-      this.collapsed = !this.collapsed;
-      this.panelCtrl.__collapsedQueryCache[this.target.refId] = this.collapsed;
-    }
-
-    try {
-      this.collapsedText = this.queryCtrl.getCollapsedText();
-    } catch (e) {
-      const err = e.message || e.toString();
-      this.collapsedText = 'Error: ' + err;
-    }
-  }
-
-  toggleEditorMode() {
-    if (this.canCollapse && this.collapsed) {
-      this.collapsed = false;
-    }
-
-    this.queryCtrl.toggleEditorMode();
-  }
-
-  removeQuery() {
-    if (this.panelCtrl.__collapsedQueryCache) {
-      delete this.panelCtrl.__collapsedQueryCache[this.target.refId];
-    }
-
-    this.panelCtrl.removeQuery(this.target);
-  }
-
-  duplicateQuery() {
-    const clone = angular.copy(this.target);
-    this.panelCtrl.addQuery(clone);
-  }
-
-  moveQuery(direction) {
-    this.panelCtrl.moveQuery(this.target, direction);
   }
 }
 

+ 11 - 17
public/app/features/plugins/plugin_component.ts

@@ -105,23 +105,17 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     switch (attrs.type) {
       // QueryCtrl
       case 'query-ctrl': {
-        const datasource = scope.target.datasource || scope.ctrl.panel.datasource;
-        return datasourceSrv.get(datasource).then(ds => {
-          scope.datasource = ds;
-
-          return importPluginModule(ds.meta.module).then(dsModule => {
-            return {
-              baseUrl: ds.meta.baseUrl,
-              name: 'query-ctrl-' + ds.meta.id,
-              bindings: { target: '=', panelCtrl: '=', datasource: '=' },
-              attrs: {
-                target: 'target',
-                'panel-ctrl': 'ctrl',
-                datasource: 'datasource',
-              },
-              Component: dsModule.QueryCtrl,
-            };
-          });
+        const ds = scope.ctrl.datasource;
+        return $q.when({
+          baseUrl: ds.meta.baseUrl,
+          name: 'query-ctrl-' + ds.meta.id,
+          bindings: { target: '=', panelCtrl: '=', datasource: '=' },
+          attrs: {
+            target: 'ctrl.target',
+            'panel-ctrl': 'ctrl',
+            datasource: 'ctrl.datasource',
+          },
+          Component: ds.pluginExports.QueryCtrl,
         });
       }
       // Annotations

+ 4 - 0
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -391,6 +391,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     this.paused = false;
     this.panelCtrl.refresh();
   }
+
+  getCollapsedText() {
+    return this.target.target;
+  }
 }
 
 function mapToDropdownOptions(results) {

+ 3 - 3
public/app/plugins/datasource/postgres/partials/query.editor.html

@@ -138,9 +138,9 @@
     <pre class="gf-form-pre alert alert-info">Time series:
 - return column named <i>time</i> (UTC in seconds or timestamp)
 - return column(s) with numeric datatype as values
-Optional: 
-  - return column named <i>metric</i> to represent the series name. 
-  - If multiple value columns are returned the metric column is used as prefix. 
+Optional:
+  - return column named <i>metric</i> to represent the series name.
+  - If multiple value columns are returned the metric column is used as prefix.
   - If no column named metric is found the column name of the value column is used as series name
 
 Resultsets of time series queries need to be sorted by time.

+ 1 - 0
public/app/types/plugins.ts

@@ -4,6 +4,7 @@ import { PanelProps, PanelOptionsProps } from '@grafana/ui';
 export interface PluginExports {
   Datasource?: any;
   QueryCtrl?: any;
+  QueryEditor?: any;
   ConfigCtrl?: any;
   AnnotationsQueryCtrl?: any;
   VariableQueryEditor?: any;

+ 5 - 1
public/app/types/series.ts

@@ -1,4 +1,4 @@
-import { PluginMeta } from './plugins';
+import { PluginMeta, PluginExports } from './plugins';
 import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui';
 
 export interface DataQueryResponse {
@@ -25,6 +25,10 @@ export interface DataQueryOptions {
 }
 
 export interface DataSourceApi {
+  name: string;
+  meta: PluginMeta;
+  pluginExports: PluginExports;
+
   /**
    *  min interval range
    */

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

@@ -35,6 +35,7 @@
   flex-grow: 1;
   background: $input-bg;
   margin: 0 20px 0 84px;
+  width: calc(100% - 84px);
   border-radius: 3px;
   box-shadow: $panel-editor-shadow;
   min-height: 0;

+ 95 - 12
public/sass/components/_query_editor.scss

@@ -3,12 +3,6 @@
   color: $blue;
 }
 
-.gf-form-disabled {
-  .query-keyword {
-    color: darken($blue, 20%);
-  }
-}
-
 .query-segment-operator {
   color: $orange;
 }
@@ -18,12 +12,6 @@
 }
 
 .gf-form-query {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: nowrap;
-  align-content: flex-start;
-  align-items: flex-start;
-
   .gf-form,
   .gf-form-filler {
     margin-bottom: 2px;
@@ -188,3 +176,98 @@ input[type='text'].tight-form-func-param {
 .rst-literal-block .rst-text {
   display: block;
 }
+
+.query-editor-row {
+  margin-bottom: 2px;
+
+  &:hover {
+    .query-editor-row__actions {
+      display: flex;
+    }
+  }
+
+  &--disabled {
+    .query-keyword {
+      color: darken($blue, 20%);
+    }
+  }
+
+}
+
+.query-editor-row__header {
+  display: flex;
+  padding: 4px 0px 4px 8px;
+  position: relative;
+  height: 35px;
+  background: $page-bg;
+  flex-wrap: nowrap;
+  align-items: center;
+}
+
+.query-editor-row__ref-id {
+  font-weight: $font-weight-semi-bold;
+  color: $blue;
+  font-size: $font-size-md;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+
+  i {
+    padding-right: 5px;
+    color: $text-muted;
+    position: relative;
+  }
+}
+
+.query-editor-row__collapsed-text {
+  padding: 0 10px;
+  display: flex;
+  align-items: center;
+  flex-grow: 1;
+  overflow: hidden;
+
+  > div {
+    color: $text-muted;
+    font-style: italic;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    font-size: $font-size-sm;
+    min-width: 0;
+  }
+}
+
+.query-editor-row__actions {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  color: $text-muted;
+}
+
+.query-editor-row__action {
+  margin-left: 3px;
+  background: transparent;
+  border: none;
+  box-shadow: none;
+
+  &:hover {
+    color: $text-color;
+  }
+}
+
+.query-editor-row__body {
+  margin: 0 0 10px 40px;
+  background: $page-bg;
+
+  &--collapsed {
+    display: none;
+  }
+}
+
+.query-editor-row__context-info {
+  font-style: italic;
+  font-size: $font-size-sm;
+  color: $text-muted;
+  padding-left: 10px;
+}
+