reactify annotation query editor

This commit is contained in:
Erik Sundell
2018-12-28 19:45:24 +01:00
parent 8a916757d7
commit 5ba429387f
10 changed files with 207 additions and 39 deletions
@@ -1,5 +1,6 @@
import { react2AngularDirective } from 'app/core/utils/react2angular';
import { QueryEditor } from './components/QueryEditor';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
export function registerAngularDirectives() {
react2AngularDirective('queryEditor', QueryEditor, [
@@ -9,4 +10,10 @@ export function registerAngularDirectives() {
['events', { watchDepth: 'reference' }],
['datasource', { watchDepth: 'reference' }],
]);
react2AngularDirective('annotationQueryEditor', AnnotationQueryEditor, [
'target',
'onQueryChange',
'onExecuteQuery',
['datasource', { watchDepth: 'reference' }],
]);
}
@@ -1,5 +1,6 @@
import _ from 'lodash';
import './query_filter_ctrl';
import { registerAngularDirectives } from './angular_wrappers';
export class StackdriverAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
@@ -27,5 +28,11 @@ export class StackdriverAnnotationsQueryCtrl {
this.annotation.target = this.annotation.target || {};
this.annotation.target.refId = 'annotationQuery';
_.defaultsDeep(this.annotation.target, this.defaults);
registerAngularDirectives();
this.handleQueryChange = this.handleQueryChange.bind(this);
}
handleQueryChange(target) {
Object.assign(this.annotation.target, target);
}
}
@@ -0,0 +1,108 @@
import React from 'react';
import _ from 'lodash';
import { Metrics } from './Metrics';
import { Filter } from './Filter';
import { AnnotationTarget } from '../types';
export interface Props {
onQueryChange: (target: AnnotationTarget) => void;
target: AnnotationTarget;
datasource: any;
}
interface State extends AnnotationTarget {
[key: string]: any;
}
const DefaultTarget: State = {
defaultProject: 'loading project...',
metricType: '',
filters: [],
metricKind: '',
valueType: '',
refId: 'annotationQuery',
title: '',
text: '',
};
export class AnnotationQueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
componentDidMount() {
this.setState({
...this.props.target,
});
}
handleMetricTypeChange({ valueType, metricKind, type, unit }) {
const { onQueryChange } = this.props;
this.setState(
{
metricType: type,
unit,
valueType,
metricKind,
},
() => {
onQueryChange(this.state);
}
);
}
handleChange(prop, value) {
this.setState({ [prop]: value }, () => {
this.props.onQueryChange(this.state);
});
}
render() {
const { defaultProject, metricType, filters, refId, title } = this.state;
const { datasource } = this.props;
return (
<React.Fragment>
<Metrics
defaultProject={defaultProject}
metricType={metricType}
templateSrv={datasource.templateSrv}
datasource={datasource}
onChange={value => this.handleMetricTypeChange(value)}
>
{metric => (
<React.Fragment>
<Filter
filtersChanged={value => this.handleChange('filters', value)}
filters={filters}
refId={refId}
hideGroupBys={true}
templateSrv={datasource.templateSrv}
datasource={datasource}
metricType={metric ? metric.type : ''}
/>
</React.Fragment>
)}
</Metrics>
<div className="gf-form gf-form-inline">
<div className="gf-form">
<span className="gf-form-label query-keyword width-9">Title</span>
<input
type="text"
className="gf-form-input width-20"
value={title}
onChange={e => this.handleChange('title', e.target.value)}
/>
</div>
<div className="gf-form">
<span className="gf-form-label query-keyword width-9">Text</span>
<input type="text" className="gf-form-input width-20" ng-model="ctrl.annotation.target.text" />
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
{/* <Help datasource={datasource} rawQuery={lastQuery} lastQueryError={lastQueryError} /> */}
</React.Fragment>
);
}
}
@@ -2,17 +2,20 @@ import React from 'react';
import _ from 'lodash';
import appEvents from 'app/core/app_events';
import { QueryMeta, Target } from '../types';
import { QueryMeta } from '../types';
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
import '../query_filter_ctrl';
export interface Props {
filtersChanged: (filters) => void;
groupBysChanged: (groupBys) => void;
groupBysChanged?: (groupBys) => void;
metricType: string;
templateSrv: any;
target: Target;
groupBys?: string[];
filters: string[];
datasource: any;
refId: string;
hideGroupBys: boolean;
}
interface State {
@@ -20,6 +23,12 @@ interface State {
loading: Promise<any>;
}
const defaultLabelData = {
metricLabels: {},
resourceLabels: {},
resourceTypes: [],
};
export class Filter extends React.Component<Props, State> {
element: any;
component: AngularComponent;
@@ -29,17 +38,20 @@ export class Filter extends React.Component<Props, State> {
return;
}
const { target, filtersChanged, groupBysChanged } = this.props;
const { groupBys, filters, filtersChanged, groupBysChanged, hideGroupBys } = this.props;
const loader = getAngularLoader();
const template = '<stackdriver-filter> </stackdriver-filter>';
const scopeProps = {
loading: this.loadLabels.bind(this),
loading: null,
labelData: null,
target,
groupBys,
filters,
filtersChanged,
groupBysChanged,
hideGroupBys,
};
scopeProps.loading = this.loadLabels(scopeProps);
this.component = loader.load(this.element, scopeProps, template);
}
@@ -60,11 +72,16 @@ export class Filter extends React.Component<Props, State> {
async loadLabels(scope) {
return new Promise(async resolve => {
try {
const { meta } = await this.props.datasource.getLabels(this.props.target.metricType, this.props.target.refId);
scope.labelData = meta;
if (!this.props.metricType) {
scope.labelData = defaultLabelData;
} else {
const { meta } = await this.props.datasource.getLabels(this.props.metricType, this.props.refId);
scope.labelData = meta;
}
resolve();
} catch (error) {
appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.props.target.metricType]);
appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.props.metricType]);
scope.labelData = defaultLabelData;
resolve();
}
});
@@ -19,6 +19,7 @@ interface State {
service: string;
metric: string;
metricDescriptor: any;
defaultProject: string;
}
export class Metrics extends React.Component<Props, State> {
@@ -29,6 +30,7 @@ export class Metrics extends React.Component<Props, State> {
service: '',
metric: '',
metricDescriptor: null,
defaultProject: '',
};
constructor(props) {
@@ -36,19 +38,21 @@ export class Metrics extends React.Component<Props, State> {
}
componentDidMount() {
this.getCurrentProject()
.then(this.loadMetricDescriptors.bind(this))
.then(this.initializeServiceAndMetrics.bind(this));
this.setState({ defaultProject: this.props.defaultProject }, () => {
this.getCurrentProject()
.then(this.loadMetricDescriptors.bind(this))
.then(this.initializeServiceAndMetrics.bind(this));
});
}
async getCurrentProject() {
return new Promise(async (resolve, reject) => {
try {
if (!this.props.defaultProject || this.props.defaultProject === 'loading project...') {
// this.props.defaultProject = await this.props.datasource.getDefaultProject();
await this.props.datasource.getDefaultProject();
if (!this.state.defaultProject || this.state.defaultProject === 'loading project...') {
const defaultProject = await this.props.datasource.getDefaultProject();
this.setState({ defaultProject });
}
resolve(this.props.defaultProject);
resolve(this.state.defaultProject);
} catch (error) {
// appEvents.emit('ds-request-error', error);
reject();
@@ -57,8 +61,8 @@ export class Metrics extends React.Component<Props, State> {
}
async loadMetricDescriptors() {
if (this.props.defaultProject !== 'loading project...') {
const metricDescriptors = await this.props.datasource.getMetricTypes(this.props.defaultProject);
if (this.state.defaultProject !== 'loading project...') {
const metricDescriptors = await this.props.datasource.getMetricTypes(this.state.defaultProject);
this.setState({ metricDescriptors });
return metricDescriptors;
} else {
@@ -81,6 +85,9 @@ export class Metrics extends React.Component<Props, State> {
getMetricsList(metricDescriptors) {
const selectedMetricDescriptor = this.getSelectedMetricDescriptor(this.props.metricType);
if (!selectedMetricDescriptor) {
return [];
}
const metricsByService = metricDescriptors.filter(m => m.service === selectedMetricDescriptor.service).map(m => ({
service: m.service,
value: m.type,
@@ -60,6 +60,11 @@ export class QueryEditor extends React.Component<Props, State> {
});
}
componentWillUnmount() {
this.props.events.off('data-received');
this.props.events.off('data-error');
}
onDataReceived(dataList) {
const series = dataList.find(item => item.refId === this.props.target.refId);
if (series) {
@@ -121,12 +126,14 @@ export class QueryEditor extends React.Component<Props, State> {
metricType,
crossSeriesReducer,
groupBys,
filters,
perSeriesAligner,
alignOptions,
alignmentPeriod,
aliasBy,
lastQuery,
lastQueryError,
refId,
} = this.state;
const { datasource } = this.props;
@@ -144,7 +151,10 @@ export class QueryEditor extends React.Component<Props, State> {
<Filter
filtersChanged={value => this.handleChange('filters', value)}
groupBysChanged={value => this.handleChange('groupBys', value)}
target={this.state}
filters={filters}
groupBys={groupBys}
refId={refId}
hideGroupBys={false}
templateSrv={datasource.templateSrv}
datasource={datasource}
metricType={metric ? metric.type : ''}
@@ -168,13 +178,11 @@ export class QueryEditor extends React.Component<Props, State> {
}
</Aggregations>
<AliasBy value={aliasBy} onChange={value => this.handleChange('aliasBy', value)} />
<AlignmentPeriods
templateSrv={datasource.templateSrv}
alignmentPeriod={alignmentPeriod}
onChange={value => this.handleChange('alignmentPeriod', value)}
/>
<Help datasource={datasource} rawQuery={lastQuery} lastQueryError={lastQueryError} />
</React.Fragment>
)}
@@ -5,13 +5,13 @@ export class FilterSegments {
filterSegments: any[];
removeSegment: any;
constructor(private uiSegmentSrv, private target, private getFilterKeysFunc, private getFilterValuesFunc) {}
constructor(private uiSegmentSrv, private filters, private getFilterKeysFunc, private getFilterValuesFunc) {}
buildSegmentModel() {
this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
this.filterSegments = [];
this.target.filters.forEach((f, index) => {
this.filters.forEach((f, index) => {
switch (index % 4) {
case 0:
this.filterSegments.push(this.uiSegmentSrv.newKey(f));
@@ -1,7 +1,12 @@
<stackdriver-filter target="ctrl.annotation.target" refresh="ctrl.refresh()" datasource="ctrl.datasource"
default-dropdown-value="ctrl.defaultDropdownValue" default-service-value="ctrl.defaultServiceValue" hide-group-bys="true"></stackdriver-filter>
<!-- <stackdriver-filter target="ctrl.annotation.target" refresh="ctrl.refresh()" datasource="ctrl.datasource"
default-dropdown-value="ctrl.defaultDropdownValue" default-service-value="ctrl.defaultServiceValue" hide-group-bys="true"></stackdriver-filter> -->
<annotation-query-editor
target="ctrl.annotation.target"
datasource="ctrl.datasource"
on-query-change="(ctrl.handleQueryChange)"
></annotation-query-editor>
<div class="gf-form gf-form-inline">
<!-- <div class="gf-form gf-form-inline">
<div class="gf-form">
<span class="gf-form-label query-keyword width-9">Title</span>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.annotation.target.title" />
@@ -10,10 +15,8 @@
<span class="gf-form-label query-keyword width-9">Text</span>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.annotation.target.text" />
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
</div> -->
<div class="gf-form grafana-info-box" style="padding: 0">
<pre class="gf-form-pre alert alert-info" style="margin-right: 0"><h5>Annotation Query Format</h5>
@@ -13,7 +13,8 @@ export class StackdriverFilter {
scope: {
labelData: '<',
loading: '<',
target: '=',
groupBys: '<',
filters: '<',
filtersChanged: '&',
groupBysChanged: '&',
hideGroupBys: '<',
@@ -28,19 +29,17 @@ export class StackdriverFilterCtrl {
groupBySegments: any[];
filterSegments: FilterSegments;
removeSegment: any;
target: any;
/** @ngInject */
constructor(private $scope, private uiSegmentSrv, private templateSrv, private $rootScope) {
this.$scope = $scope.$parent;
this.target = this.$scope.target;
this.initSegments(this.$scope.hideGroupBys);
}
initSegments(hideGroupBys: boolean) {
if (!hideGroupBys) {
this.groupBySegments = this.target.groupBys.map(groupBy => {
this.groupBySegments = this.$scope.groupBys.map(groupBy => {
return this.uiSegmentSrv.getSegmentForValue(groupBy);
});
this.ensurePlusButton(this.groupBySegments);
@@ -50,7 +49,7 @@ export class StackdriverFilterCtrl {
this.filterSegments = new FilterSegments(
this.uiSegmentSrv,
this.target,
this.$scope.filters,
this.getFilterKeys.bind(this),
this.getFilterValues.bind(this)
);
@@ -93,7 +92,7 @@ export class StackdriverFilterCtrl {
async getFilterKeys(segment, removeText?: string) {
let elements = await this.createLabelKeyElements();
if (this.target.filters.indexOf(this.resourceTypeValue) !== -1) {
if (this.$scope.filters.indexOf(this.resourceTypeValue) !== -1) {
elements = elements.filter(e => e.value !== this.resourceTypeValue);
}
@@ -111,7 +110,7 @@ export class StackdriverFilterCtrl {
async getGroupBys(segment) {
let elements = await this.createLabelKeyElements();
elements = elements.filter(e => this.target.groupBys.indexOf(e.value) === -1);
elements = elements.filter(e => this.$scope.groupBys.indexOf(e.value) === -1);
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
if (noValueOrPlusButton && elements.length === 0) {
return [];
@@ -142,6 +141,7 @@ export class StackdriverFilterCtrl {
}
async getFilters(segment, index) {
await this.$scope.loading;
const hasNoFilterKeys =
this.$scope.labelData.metricLabels && Object.keys(this.$scope.labelData.metricLabels).length === 0;
return this.filterSegments.getFilters(segment, index, hasNoFilterKeys);
@@ -32,8 +32,19 @@ export interface Target {
groupBys: string[];
filters: string[];
aliasBy: string;
metricKind: any;
valueType: any;
metricKind: string;
valueType: string;
}
export interface AnnotationTarget {
defaultProject: string;
metricType: string;
refId: string;
filters: string[];
metricKind: string;
valueType: string;
title: string;
text: string;
}
export interface QueryMeta {