mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14930 from grafana/react-query-editor
React query editor (part1)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
Normal file
237
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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-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> -->
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -391,6 +391,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
this.paused = false;
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
|
||||
getCollapsedText() {
|
||||
return this.target.target;
|
||||
}
|
||||
}
|
||||
|
||||
function mapToDropdownOptions(results) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user