Commit dfe1b20f by Torkel Ödegaard Committed by GitHub

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

React query editor (part1)
parents 6f6c4652 3cb73e79
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import _ from 'lodash';
import Scrollbars from 'react-custom-scrollbars'; import Scrollbars from 'react-custom-scrollbars';
interface Props { interface Props {
...@@ -8,6 +9,8 @@ interface Props { ...@@ -8,6 +9,8 @@ interface Props {
autoHideDuration?: number; autoHideDuration?: number;
autoMaxHeight?: string; autoMaxHeight?: string;
hideTracksWhenNotNeeded?: boolean; hideTracksWhenNotNeeded?: boolean;
scrollTop?: number;
setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
autoHeightMin?: number | string; autoHeightMin?: number | string;
} }
...@@ -22,14 +25,44 @@ export class CustomScrollbar extends PureComponent<Props> { ...@@ -22,14 +25,44 @@ export class CustomScrollbar extends PureComponent<Props> {
autoHideDuration: 200, autoHideDuration: 200,
autoMaxHeight: '100%', autoMaxHeight: '100%',
hideTracksWhenNotNeeded: false, hideTracksWhenNotNeeded: false,
scrollTop: 0,
setScrollTop: () => {},
autoHeightMin: '0' 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() { render() {
const { customClassName, children, autoMaxHeight } = this.props; const { customClassName, children, autoMaxHeight } = this.props;
return ( return (
<Scrollbars <Scrollbars
ref={this.ref}
className={customClassName} className={customClassName}
autoHeight={true} autoHeight={true}
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently. // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
......
...@@ -10,6 +10,8 @@ interface Props { ...@@ -10,6 +10,8 @@ interface Props {
heading: string; heading: string;
renderToolbar?: () => JSX.Element; renderToolbar?: () => JSX.Element;
toolbarItems?: EditorToolbarView[]; toolbarItems?: EditorToolbarView[];
scrollTop?: number;
setScrollTop?: (value: React.MouseEvent<HTMLElement>) => void;
} }
export interface EditorToolbarView { export interface EditorToolbarView {
...@@ -103,7 +105,7 @@ export class EditorTabBody extends PureComponent<Props, State> { ...@@ -103,7 +105,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
} }
render() { render() {
const { children, renderToolbar, heading, toolbarItems } = this.props; const { children, renderToolbar, heading, toolbarItems, scrollTop, setScrollTop } = this.props;
const { openView, fadeIn, isOpen } = this.state; const { openView, fadeIn, isOpen } = this.state;
return ( return (
...@@ -119,7 +121,7 @@ export class EditorTabBody extends PureComponent<Props, State> { ...@@ -119,7 +121,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
)} )}
</div> </div>
<div className="panel-editor__scroll"> <div className="panel-editor__scroll">
<CustomScrollbar autoHide={false}> <CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
<div className="panel-editor__content"> <div className="panel-editor__content">
<FadeIn in={isOpen} duration={200} unmountOnExit={true}> <FadeIn in={isOpen} duration={200} unmountOnExit={true}>
{openView && this.renderOpenView(openView)} {openView && this.renderOpenView(openView)}
......
...@@ -3,18 +3,16 @@ import React, { PureComponent } from 'react'; ...@@ -3,18 +3,16 @@ import React, { PureComponent } from 'react';
import _ from 'lodash'; import _ from 'lodash';
// Components // Components
import 'app/features/panel/metrics_tab';
import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector'; import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions'; import { QueryOptions } from './QueryOptions';
import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab';
import { PanelOptionsGroup } from '@grafana/ui'; import { PanelOptionsGroup } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
// Services // Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
import config from 'app/core/config'; import config from 'app/core/config';
// Types // Types
...@@ -34,66 +32,27 @@ interface State { ...@@ -34,66 +32,27 @@ interface State {
isLoadingHelp: boolean; isLoadingHelp: boolean;
isPickerOpen: boolean; isPickerOpen: boolean;
isAddingMixed: boolean; isAddingMixed: boolean;
scrollTop: number;
} }
export class QueriesTab extends PureComponent<Props, State> { export class QueriesTab extends PureComponent<Props, State> {
element: HTMLElement;
component: AngularComponent;
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
backendSrv: BackendSrv = getBackendSrv(); backendSrv: BackendSrv = getBackendSrv();
constructor(props) { state: State = {
super(props); isLoadingHelp: false,
currentDS: this.findCurrentDataSource(),
this.state = { helpContent: null,
isLoadingHelp: false, isPickerOpen: false,
currentDS: this.findCurrentDataSource(), isAddingMixed: false,
helpContent: null, scrollTop: 0,
isPickerOpen: false, };
isAddingMixed: false,
};
}
findCurrentDataSource(): DataSourceSelectItem { findCurrentDataSource(): DataSourceSelectItem {
const { panel } = this.props; const { panel } = this.props;
return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; 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 => { onChangeDataSource = datasource => {
const { panel } = this.props; const { panel } = this.props;
const { currentDS } = this.state; const { currentDS } = this.state;
...@@ -137,7 +96,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -137,7 +96,7 @@ export class QueriesTab extends PureComponent<Props, State> {
onAddQuery = (query?: Partial<DataQuery>) => { onAddQuery = (query?: Partial<DataQuery>) => {
this.props.panel.addQuery(query); this.props.panel.addQuery(query);
this.forceUpdate(); this.setState({ scrollTop: this.state.scrollTop + 100000 });
}; };
onAddQueryClick = () => { onAddQueryClick = () => {
...@@ -146,9 +105,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -146,9 +105,7 @@ export class QueriesTab extends PureComponent<Props, State> {
return; return;
} }
this.props.panel.addQuery(); this.onAddQuery();
this.component.digest();
this.forceUpdate();
}; };
onRemoveQuery = (query: DataQuery) => { onRemoveQuery = (query: DataQuery) => {
...@@ -171,9 +128,21 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -171,9 +128,21 @@ export class QueriesTab extends PureComponent<Props, State> {
}; };
renderToolbar = () => { 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 = () => { renderMixedPicker = () => {
...@@ -190,17 +159,21 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -190,17 +159,21 @@ export class QueriesTab extends PureComponent<Props, State> {
onAddMixedQuery = datasource => { onAddMixedQuery = datasource => {
this.onAddQuery({ datasource: datasource.name }); this.onAddQuery({ datasource: datasource.name });
this.component.digest(); this.setState({ isAddingMixed: false, scrollTop: this.state.scrollTop + 10000 });
this.setState({ isAddingMixed: false });
}; };
onMixedPickerBlur = () => { onMixedPickerBlur = () => {
this.setState({ isAddingMixed: false }); this.setState({ isAddingMixed: false });
}; };
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement;
this.setState({ scrollTop: target.scrollTop });
};
render() { render() {
const { panel } = this.props; const { panel } = this.props;
const { currentDS, isAddingMixed } = this.state; const { currentDS, scrollTop } = this.state;
const queryInspector: EditorToolbarView = { const queryInspector: EditorToolbarView = {
title: 'Query Inspector', title: 'Query Inspector',
...@@ -214,32 +187,28 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -214,32 +187,28 @@ export class QueriesTab extends PureComponent<Props, State> {
}; };
return ( 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 className="query-editor-rows"> {panel.targets.map((query, index) => (
<div ref={element => (this.element = element)} /> <QueryEditorRow
datasourceName={query.datasource || panel.datasource}
<div className="gf-form-query"> key={query.refId}
<div className="gf-form gf-form-query-letter-cell"> panel={panel}
<label className="gf-form-label"> query={query}
<span className="gf-form-query-letter-cell-carret muted"> onRemoveQuery={this.onRemoveQuery}
<i className="fa fa-caret-down" /> onAddQuery={this.onAddQuery}
</span>{' '} onMoveQuery={this.onMoveQuery}
<span className="gf-form-query-letter-cell-letter">{panel.getNextQueryLetter()}</span> inMixedMode={currentDS.meta.mixed}
</label> />
</div> ))}
<div className="gf-form"> </div>
{!isAddingMixed && (
<button className="btn btn-secondary gf-form-btn" onClick={this.onAddQueryClick}>
Add Query
</button>
)}
{isAddingMixed && this.renderMixedPicker()}
</div>
</div>
</div>
</PanelOptionsGroup>
<PanelOptionsGroup> <PanelOptionsGroup>
<QueryOptions panel={panel} datasource={currentDS} /> <QueryOptions panel={panel} datasource={currentDS} />
</PanelOptionsGroup> </PanelOptionsGroup>
......
// 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;
}
...@@ -26,6 +26,7 @@ interface Props { ...@@ -26,6 +26,7 @@ interface Props {
interface State { interface State {
isVizPickerOpen: boolean; isVizPickerOpen: boolean;
searchQuery: string; searchQuery: string;
scrollTop: number;
} }
export class VisualizationTab extends PureComponent<Props, State> { export class VisualizationTab extends PureComponent<Props, State> {
...@@ -39,6 +40,7 @@ export class VisualizationTab extends PureComponent<Props, State> { ...@@ -39,6 +40,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
this.state = { this.state = {
isVizPickerOpen: false, isVizPickerOpen: false,
searchQuery: '', searchQuery: '',
scrollTop: 0,
}; };
} }
...@@ -143,7 +145,7 @@ export class VisualizationTab extends PureComponent<Props, State> { ...@@ -143,7 +145,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
}; };
onOpenVizPicker = () => { onOpenVizPicker = () => {
this.setState({ isVizPickerOpen: true }); this.setState({ isVizPickerOpen: true, scrollTop: 0 });
}; };
onCloseVizPicker = () => { onCloseVizPicker = () => {
...@@ -201,9 +203,14 @@ export class VisualizationTab extends PureComponent<Props, State> { ...@@ -201,9 +203,14 @@ export class VisualizationTab extends PureComponent<Props, State> {
renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />; 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() { render() {
const { plugin } = this.props; const { plugin } = this.props;
const { isVizPickerOpen, searchQuery } = this.state; const { isVizPickerOpen, searchQuery, scrollTop } = this.state;
const pluginHelp: EditorToolbarView = { const pluginHelp: EditorToolbarView = {
heading: 'Help', heading: 'Help',
...@@ -212,7 +219,8 @@ export class VisualizationTab extends PureComponent<Props, State> { ...@@ -212,7 +219,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
}; };
return ( 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}> <FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
<VizTypePicker <VizTypePicker
......
// 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);
<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> -->
<div class="gf-form-query"> <div ng-transclude class="gf-form-query-content"></div>
<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 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>
...@@ -3,89 +3,26 @@ import angular from 'angular'; ...@@ -3,89 +3,26 @@ import angular from 'angular';
const module = angular.module('grafana.directives'); const module = angular.module('grafana.directives');
export class QueryRowCtrl { export class QueryRowCtrl {
collapsedText: string;
canCollapse: boolean;
getCollapsedText: any;
target: any; target: any;
queryCtrl: any; queryCtrl: any;
panelCtrl: any; panelCtrl: any;
panel: any; panel: any;
collapsed: any; hasTextEditMode: boolean;
hideEditorRowActions: boolean;
constructor() { constructor() {
this.panelCtrl = this.queryCtrl.panelCtrl; this.panelCtrl = this.queryCtrl.panelCtrl;
this.target = this.queryCtrl.target; this.target = this.queryCtrl.target;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.hideEditorRowActions = this.panelCtrl.hideEditorRowActions;
if (!this.target.refId) { if (this.hasTextEditMode) {
this.target.refId = this.panel.getNextQueryLetter(); // expose this function to react parent component
this.panelCtrl.toggleEditorMode = this.queryCtrl.toggleEditorMode.bind(this.queryCtrl);
} }
this.toggleCollapse(true); if (this.queryCtrl.getCollapsedText) {
if (this.target.isNew) { // expose this function to react parent component
delete this.target.isNew; this.panelCtrl.getCollapsedText = this.queryCtrl.getCollapsedText.bind(this.queryCtrl);
this.toggleCollapse(false);
} }
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);
} }
} }
......
...@@ -105,23 +105,17 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ ...@@ -105,23 +105,17 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
switch (attrs.type) { switch (attrs.type) {
// QueryCtrl // QueryCtrl
case 'query-ctrl': { case 'query-ctrl': {
const datasource = scope.target.datasource || scope.ctrl.panel.datasource; const ds = scope.ctrl.datasource;
return datasourceSrv.get(datasource).then(ds => { return $q.when({
scope.datasource = ds; baseUrl: ds.meta.baseUrl,
name: 'query-ctrl-' + ds.meta.id,
return importPluginModule(ds.meta.module).then(dsModule => { bindings: { target: '=', panelCtrl: '=', datasource: '=' },
return { attrs: {
baseUrl: ds.meta.baseUrl, target: 'ctrl.target',
name: 'query-ctrl-' + ds.meta.id, 'panel-ctrl': 'ctrl',
bindings: { target: '=', panelCtrl: '=', datasource: '=' }, datasource: 'ctrl.datasource',
attrs: { },
target: 'target', Component: ds.pluginExports.QueryCtrl,
'panel-ctrl': 'ctrl',
datasource: 'datasource',
},
Component: dsModule.QueryCtrl,
};
});
}); });
} }
// Annotations // Annotations
......
...@@ -391,6 +391,10 @@ export class GraphiteQueryCtrl extends QueryCtrl { ...@@ -391,6 +391,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
this.paused = false; this.paused = false;
this.panelCtrl.refresh(); this.panelCtrl.refresh();
} }
getCollapsedText() {
return this.target.target;
}
} }
function mapToDropdownOptions(results) { function mapToDropdownOptions(results) {
......
...@@ -138,9 +138,9 @@ ...@@ -138,9 +138,9 @@
<pre class="gf-form-pre alert alert-info">Time series: <pre class="gf-form-pre alert alert-info">Time series:
- return column named <i>time</i> (UTC in seconds or timestamp) - return column named <i>time</i> (UTC in seconds or timestamp)
- return column(s) with numeric datatype as values - return column(s) with numeric datatype as values
Optional: Optional:
- return column named <i>metric</i> to represent the series name. - 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 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 - 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. Resultsets of time series queries need to be sorted by time.
......
...@@ -4,6 +4,7 @@ import { PanelProps, PanelOptionsProps } from '@grafana/ui'; ...@@ -4,6 +4,7 @@ import { PanelProps, PanelOptionsProps } from '@grafana/ui';
export interface PluginExports { export interface PluginExports {
Datasource?: any; Datasource?: any;
QueryCtrl?: any; QueryCtrl?: any;
QueryEditor?: any;
ConfigCtrl?: any; ConfigCtrl?: any;
AnnotationsQueryCtrl?: any; AnnotationsQueryCtrl?: any;
VariableQueryEditor?: any; VariableQueryEditor?: any;
......
import { PluginMeta } from './plugins'; import { PluginMeta, PluginExports } from './plugins';
import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui'; import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui';
export interface DataQueryResponse { export interface DataQueryResponse {
...@@ -25,6 +25,10 @@ export interface DataQueryOptions { ...@@ -25,6 +25,10 @@ export interface DataQueryOptions {
} }
export interface DataSourceApi { export interface DataSourceApi {
name: string;
meta: PluginMeta;
pluginExports: PluginExports;
/** /**
* min interval range * min interval range
*/ */
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
flex-grow: 1; flex-grow: 1;
background: $input-bg; background: $input-bg;
margin: 0 20px 0 84px; margin: 0 20px 0 84px;
width: calc(100% - 84px);
border-radius: 3px; border-radius: 3px;
box-shadow: $panel-editor-shadow; box-shadow: $panel-editor-shadow;
min-height: 0; min-height: 0;
......
...@@ -3,12 +3,6 @@ ...@@ -3,12 +3,6 @@
color: $blue; color: $blue;
} }
.gf-form-disabled {
.query-keyword {
color: darken($blue, 20%);
}
}
.query-segment-operator { .query-segment-operator {
color: $orange; color: $orange;
} }
...@@ -18,12 +12,6 @@ ...@@ -18,12 +12,6 @@
} }
.gf-form-query { .gf-form-query {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: flex-start;
align-items: flex-start;
.gf-form, .gf-form,
.gf-form-filler { .gf-form-filler {
margin-bottom: 2px; margin-bottom: 2px;
...@@ -188,3 +176,98 @@ input[type='text'].tight-form-func-param { ...@@ -188,3 +176,98 @@ input[type='text'].tight-form-func-param {
.rst-literal-block .rst-text { .rst-literal-block .rst-text {
display: block; 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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment