mirror of https://github.com/grafana/grafana
parent
ad55be9865
commit
146aa7abab
@ -0,0 +1,48 @@ |
||||
import { react2AngularDirective } from 'app/core/utils/react2angular'; |
||||
import { QueryEditor } from './components/QueryEditor'; |
||||
// import { MetricPicker } from './components/MetricPicker';
|
||||
// import { OptionPicker } from './components/OptionPicker';
|
||||
// import { OptionGroupPicker } from './components/OptionGroupPicker';
|
||||
// import { AggregationPicker } from './components/AggregationPicker';
|
||||
|
||||
export function registerAngularDirectives() { |
||||
// react2AngularDirective('optionPicker', OptionPicker, [
|
||||
// 'options',
|
||||
// 'onChange',
|
||||
// 'selected',
|
||||
// 'searchable',
|
||||
// 'className',
|
||||
// 'placeholder',
|
||||
// ]);
|
||||
// react2AngularDirective('optionGroupPicker', OptionGroupPicker, [
|
||||
// 'groups',
|
||||
// 'onChange',
|
||||
// 'selected',
|
||||
// 'searchable',
|
||||
// 'className',
|
||||
// 'placeholder',
|
||||
// ]);
|
||||
// react2AngularDirective('metricPicker', MetricPicker, [
|
||||
// 'target',
|
||||
// ['onChange', { watchDepth: 'reference' }],
|
||||
// 'defaultProject',
|
||||
// 'metricType',
|
||||
// ['templateSrv', { watchDepth: 'reference' }],
|
||||
// ['datasource', { watchDepth: 'reference' }],
|
||||
// ]);
|
||||
// react2AngularDirective('aggregationPicker', AggregationPicker, [
|
||||
// 'valueType',
|
||||
// 'metricKind',
|
||||
// 'onChange',
|
||||
// 'aggregation',
|
||||
// ['templateSrv', { watchDepth: 'reference' }],
|
||||
// ]);
|
||||
|
||||
react2AngularDirective('queryEditor', QueryEditor, [ |
||||
'target', |
||||
'onChange', |
||||
['uiSegmentSrv', { watchDepth: 'reference' }], |
||||
['datasource', { watchDepth: 'reference' }], |
||||
['templateSrv', { watchDepth: 'reference' }], |
||||
]); |
||||
} |
||||
@ -0,0 +1,134 @@ |
||||
import React from 'react'; |
||||
import _ from 'lodash'; |
||||
|
||||
// import { OptionPicker } from './OptionPicker';
|
||||
import { OptionGroupPicker } from './OptionGroupPicker'; |
||||
// import { alignmentPeriods } from '../constants';
|
||||
// import { getAlignmentOptionsByMetric, getAggregationOptionsByMetric } from '../functions';
|
||||
import { getAggregationOptionsByMetric } from '../functions'; |
||||
// import kbn from 'app/core/utils/kbn';
|
||||
|
||||
export interface Props { |
||||
onChange: (metricDescriptor) => void; |
||||
templateSrv: any; |
||||
valueType: string; |
||||
metricKind: string; |
||||
aggregation: { |
||||
crossSeriesReducer: string; |
||||
alignmentPeriod: string; |
||||
perSeriesAligner: string; |
||||
groupBys: string[]; |
||||
}; |
||||
} |
||||
|
||||
interface State { |
||||
alignmentPeriods: any[]; |
||||
alignOptions: any[]; |
||||
aggOptions: any[]; |
||||
} |
||||
|
||||
export class AggregationPicker extends React.Component<Props, State> { |
||||
state: State = { |
||||
alignmentPeriods: [], |
||||
alignOptions: [], |
||||
aggOptions: [], |
||||
}; |
||||
|
||||
constructor(props) { |
||||
super(props); |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.setAggOptions(); |
||||
} |
||||
|
||||
componentWillReceiveProps(nextProps: Props) { |
||||
const { valueType, metricKind, aggregation } = this.props; |
||||
if ( |
||||
nextProps.valueType !== valueType || |
||||
nextProps.metricKind !== metricKind || |
||||
nextProps.aggregation.groupBys !== aggregation.groupBys |
||||
) { |
||||
this.setAggOptions(); |
||||
} |
||||
} |
||||
|
||||
setAggOptions() { |
||||
const { valueType, metricKind, aggregation, templateSrv } = this.props; |
||||
let aggregations = getAggregationOptionsByMetric(valueType, metricKind).map(a => ({ |
||||
...a, |
||||
label: a.text, |
||||
})); |
||||
if (!aggregations.find(o => o.value === templateSrv.replace(aggregation.crossSeriesReducer))) { |
||||
this.deselectAggregationOption('REDUCE_NONE'); |
||||
} |
||||
|
||||
if (aggregation.groupBys.length > 0) { |
||||
aggregations = aggregations.filter(o => o.value !== 'REDUCE_NONE'); |
||||
this.deselectAggregationOption('REDUCE_NONE'); |
||||
} |
||||
this.setState({ |
||||
aggOptions: [ |
||||
this.getTemplateVariablesGroup(), |
||||
{ |
||||
label: 'Aggregations', |
||||
options: aggregations, |
||||
}, |
||||
], |
||||
}); |
||||
} |
||||
|
||||
deselectAggregationOption(notValidOptionValue: string) { |
||||
const aggregations = getAggregationOptionsByMetric(this.props.valueType, this.props.metricKind); |
||||
const newValue = aggregations.find(o => o.value !== notValidOptionValue); |
||||
this.handleAggregationChange(newValue ? newValue.value : ''); |
||||
} |
||||
|
||||
handleAggregationChange(value) { |
||||
this.props.onChange(value); |
||||
// this.$scope.refresh();
|
||||
} |
||||
|
||||
getTemplateVariablesGroup() { |
||||
return { |
||||
label: 'Template Variables', |
||||
options: this.props.templateSrv.variables.map(v => ({ |
||||
label: `$${v.name}`, |
||||
value: `$${v.name}`, |
||||
})), |
||||
}; |
||||
} |
||||
|
||||
render() { |
||||
const { aggOptions } = this.state; |
||||
const { aggregation } = this.props; |
||||
|
||||
return ( |
||||
<React.Fragment> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<label className="gf-form-label query-keyword width-9">Aggregation</label> |
||||
<div className="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> |
||||
<OptionGroupPicker |
||||
onChange={value => this.handleAggregationChange(value)} |
||||
selected={aggregation.crossSeriesReducer} |
||||
groups={aggOptions} |
||||
searchable={true} |
||||
placeholder="Select Aggregation" |
||||
className="width-15" |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div className="gf-form gf-form--grow"> |
||||
<label className="gf-form-label gf-form-label--grow"> |
||||
<a ng-click="ctrl.target.showAggregationOptions = !ctrl.target.showAggregationOptions"> |
||||
<i className="fa fa-caret-down" ng-show="ctrl.target.showAggregationOptions" /> |
||||
<i className="fa fa-caret-right" ng-hide="ctrl.target.showAggregationOptions" /> Advanced Options |
||||
</a> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
</React.Fragment> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,239 @@ |
||||
import React from 'react'; |
||||
import _ from 'lodash'; |
||||
|
||||
import Segment from './Segment'; |
||||
import { QueryMeta, Target } from '../types'; |
||||
import { FilterSegments } from '../filter_segments'; |
||||
|
||||
export interface Props { |
||||
onChange: (metricDescriptor) => void; |
||||
templateSrv: any; |
||||
labelData: QueryMeta; |
||||
loading: Promise<any>; |
||||
target: Target; |
||||
uiSegmentSrv: any; |
||||
} |
||||
|
||||
interface State { |
||||
defaultRemoveGroupByValue: string; |
||||
resourceTypeValue: string; |
||||
groupBySegments: any[]; |
||||
// filterSegments: FilterSegments;
|
||||
filterSegments: any; |
||||
removeSegment?: any; |
||||
} |
||||
|
||||
export class Filter extends React.Component<Props, State> { |
||||
state: State = { |
||||
defaultRemoveGroupByValue: '-- remove group by --', |
||||
resourceTypeValue: 'resource.type', |
||||
groupBySegments: [], |
||||
filterSegments: new FilterSegments(this.getFilterKeys.bind(this), this.getFilterValues.bind(this)), |
||||
}; |
||||
|
||||
constructor(props) { |
||||
super(props); |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.initSegments(false); |
||||
} |
||||
|
||||
shouldComponentUpdate(nextProps) { |
||||
return this.state.filterSegments.filterSegments.length > 0; |
||||
} |
||||
|
||||
initSegments(hideGroupBys: boolean) { |
||||
this.state.filterSegments.init(this.props.uiSegmentSrv); |
||||
if (!hideGroupBys) { |
||||
this.setState({ |
||||
groupBySegments: this.props.target.aggregation.groupBys.map(groupBy => { |
||||
return this.props.uiSegmentSrv.getSegmentForValue(groupBy); |
||||
}), |
||||
}); |
||||
|
||||
this.ensurePlusButton(this.state.groupBySegments); |
||||
} |
||||
|
||||
this.setState({ |
||||
removeSegment: this.props.uiSegmentSrv.newSegment({ fake: true, value: '-- remove group by --' }), |
||||
}); |
||||
|
||||
this.state.filterSegments.buildSegmentModel(this.props.target.filters); |
||||
} |
||||
|
||||
async createLabelKeyElements() { |
||||
await this.props.loading; |
||||
|
||||
let elements = Object.keys(this.props.labelData.metricLabels || {}).map(l => { |
||||
return this.props.uiSegmentSrv.newSegment({ |
||||
value: `metric.label.${l}`, |
||||
expandable: false, |
||||
}); |
||||
}); |
||||
|
||||
elements = [ |
||||
...elements, |
||||
...Object.keys(this.props.labelData.resourceLabels || {}).map(l => { |
||||
return this.props.uiSegmentSrv.newSegment({ |
||||
value: `resource.label.${l}`, |
||||
expandable: false, |
||||
}); |
||||
}), |
||||
]; |
||||
|
||||
if (this.props.labelData.resourceTypes && this.props.labelData.resourceTypes.length > 0) { |
||||
elements = [ |
||||
...elements, |
||||
this.props.uiSegmentSrv.newSegment({ |
||||
value: this.state.resourceTypeValue, |
||||
expandable: false, |
||||
}), |
||||
]; |
||||
} |
||||
|
||||
return elements; |
||||
} |
||||
|
||||
async getFilterKeys(segment, removeText?: string) { |
||||
let elements = await this.createLabelKeyElements(); |
||||
|
||||
if (this.props.target.filters.indexOf(this.state.resourceTypeValue) !== -1) { |
||||
elements = elements.filter(e => e.value !== this.state.resourceTypeValue); |
||||
} |
||||
|
||||
const noValueOrPlusButton = !segment || segment.type === 'plus-button'; |
||||
if (noValueOrPlusButton && elements.length === 0) { |
||||
return []; |
||||
} |
||||
|
||||
return [ |
||||
...elements, |
||||
this.props.uiSegmentSrv.newSegment({ fake: true, value: removeText || this.state.defaultRemoveGroupByValue }), |
||||
]; |
||||
} |
||||
|
||||
async getGroupBys(segment) { |
||||
let elements = await this.createLabelKeyElements(); |
||||
|
||||
elements = elements.filter(e => this.props.target.aggregation.groupBys.indexOf(e.value) === -1); |
||||
const noValueOrPlusButton = !segment || segment.type === 'plus-button'; |
||||
if (noValueOrPlusButton && elements.length === 0) { |
||||
return []; |
||||
} |
||||
|
||||
this.state.removeSegment.value = this.state.defaultRemoveGroupByValue; |
||||
return [...elements, this.state.removeSegment]; |
||||
} |
||||
|
||||
groupByChanged(segment, index) { |
||||
if (segment.value === this.state.removeSegment.value) { |
||||
// this.groupBySegments.splice(index, 1);
|
||||
} else { |
||||
segment.type = 'value'; |
||||
} |
||||
|
||||
const reducer = (memo, seg) => { |
||||
if (!seg.fake) { |
||||
memo.push(seg.value); |
||||
} |
||||
return memo; |
||||
}; |
||||
|
||||
this.props.target.aggregation.groupBys = this.state.groupBySegments.reduce(reducer, []); |
||||
this.ensurePlusButton(this.state.groupBySegments); |
||||
// this.$rootScope.$broadcast('metricTypeChanged');
|
||||
// this.$scope.refresh();
|
||||
} |
||||
|
||||
async getFilters(segment, index) { |
||||
await this.props.loading; |
||||
const hasNoFilterKeys = |
||||
this.props.labelData.metricLabels && Object.keys(this.props.labelData.metricLabels).length === 0; |
||||
return this.state.filterSegments.getFilters(segment, index, hasNoFilterKeys); |
||||
} |
||||
|
||||
getFilterValues(index) { |
||||
const filterKey = this.props.templateSrv.replace(this.state.filterSegments.filterSegments[index - 2].value); |
||||
if ( |
||||
!filterKey || |
||||
!this.props.labelData.metricLabels || |
||||
Object.keys(this.props.labelData.metricLabels).length === 0 |
||||
) { |
||||
return []; |
||||
} |
||||
|
||||
const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7); |
||||
|
||||
if (filterKey.startsWith('metric.label.') && this.props.labelData.metricLabels.hasOwnProperty(shortKey)) { |
||||
return this.props.labelData.metricLabels[shortKey]; |
||||
} |
||||
|
||||
if (filterKey.startsWith('resource.label.') && this.props.labelData.resourceLabels.hasOwnProperty(shortKey)) { |
||||
return this.props.labelData.resourceLabels[shortKey]; |
||||
} |
||||
|
||||
if (filterKey === this.state.resourceTypeValue) { |
||||
return this.props.labelData.resourceTypes; |
||||
} |
||||
|
||||
return []; |
||||
} |
||||
|
||||
filterSegmentUpdated(segment, index) { |
||||
this.props.target.filters = this.state.filterSegments.filterSegmentUpdated(segment, index); |
||||
// this.$scope.refresh();
|
||||
} |
||||
|
||||
ensurePlusButton(segments) { |
||||
const count = segments.length; |
||||
const lastSegment = segments[Math.max(count - 1, 0)]; |
||||
|
||||
if (!lastSegment || lastSegment.type !== 'plus-button') { |
||||
segments.push(this.props.uiSegmentSrv.newPlusButton()); |
||||
} |
||||
} |
||||
|
||||
render() { |
||||
const { filterSegments } = this.state; |
||||
// const { metrifilterSegmentscType } = this.props;
|
||||
|
||||
return ( |
||||
<React.Fragment> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<span className="gf-form-label query-keyword width-9">Filter</span> |
||||
<div className="gf-form"> |
||||
{filterSegments.filterSegments.map((segment, i) => ( |
||||
<Segment |
||||
key={i} |
||||
segment={segment} |
||||
getOptions={() => this.getFilters(segment, i)} |
||||
onChange={segment => this.filterSegmentUpdated(segment, i)} |
||||
/> |
||||
))} |
||||
</div> |
||||
</div> |
||||
<div className="gf-form gf-form--grow"> |
||||
<div className="gf-form-label gf-form-label--grow" /> |
||||
</div> |
||||
</div> |
||||
{/* <div className="gf-form-inline" ng-hide="ctrl.$scope.hideGroupBys"> |
||||
<div className="gf-form"> |
||||
<span className="gf-form-label query-keyword width-9">Group By</span> |
||||
<div className="gf-form" ng-repeat="segment in ctrl.groupBySegments"> |
||||
<Segment |
||||
segment="segment" |
||||
get-options="ctrl.getGroupBys(segment)" |
||||
on-change="ctrl.groupByChanged(segment, $index)" |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div className="gf-form gf-form--grow"> |
||||
<div className="gf-form-label gf-form-label--grow" /> |
||||
</div> |
||||
</div> */} |
||||
</React.Fragment> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,141 @@ |
||||
import React from 'react'; |
||||
import _ from 'lodash'; |
||||
import appEvents from 'app/core/app_events'; |
||||
|
||||
import { MetricPicker } from './MetricPicker'; |
||||
import { Filter } from './Filter'; |
||||
// import { AggregationPicker } from './AggregationPicker';
|
||||
import { Target, QueryMeta } from '../types'; |
||||
|
||||
export interface Props { |
||||
onChange: (target: Target) => void; |
||||
target: Target; |
||||
datasource: any; |
||||
templateSrv: any; |
||||
uiSegmentSrv: any; |
||||
} |
||||
|
||||
interface State { |
||||
target: Target; |
||||
labelData: QueryMeta; |
||||
loadLabelsPromise: Promise<any>; |
||||
} |
||||
|
||||
const DefaultTarget: Target = { |
||||
defaultProject: 'loading project...', |
||||
metricType: '', |
||||
refId: '', |
||||
service: '', |
||||
unit: '', |
||||
aggregation: { |
||||
crossSeriesReducer: 'REDUCE_MEAN', |
||||
alignmentPeriod: 'stackdriver-auto', |
||||
perSeriesAligner: 'ALIGN_MEAN', |
||||
groupBys: [], |
||||
}, |
||||
filters: [], |
||||
aliasBy: '', |
||||
metricKind: '', |
||||
valueType: '', |
||||
}; |
||||
|
||||
export class QueryEditor extends React.Component<Props, State> { |
||||
state: State = { labelData: null, loadLabelsPromise: null, target: DefaultTarget }; |
||||
|
||||
constructor(props) { |
||||
super(props); |
||||
this.handleMetricTypeChange = this.handleMetricTypeChange.bind(this); |
||||
this.handleAggregationChange = this.handleAggregationChange.bind(this); |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.setState({ target: this.props.target }); |
||||
this.getLabels(); |
||||
} |
||||
|
||||
async getLabels() { |
||||
const loadLabelsPromise = new Promise(async resolve => { |
||||
try { |
||||
const { meta } = await this.props.datasource.getLabels(this.props.target.metricType, this.props.target.refId); |
||||
this.setState({ labelData: meta }); |
||||
resolve(); |
||||
} catch (error) { |
||||
appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.props.target.metricType]); |
||||
resolve(); |
||||
} |
||||
}); |
||||
this.setState({ loadLabelsPromise }); |
||||
} |
||||
|
||||
handleMetricTypeChange({ valueType, metricKind, type, unit }) { |
||||
this.setState({ |
||||
target: { |
||||
...this.state.target, |
||||
...{ |
||||
metricType: type, |
||||
unit, |
||||
valueType, |
||||
metricKind, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
// this.$rootScope.$broadcast('metricTypeChanged');
|
||||
// this.getLabels();
|
||||
// this.refresh();
|
||||
} |
||||
|
||||
handleAggregationChange(crossSeriesReducer) { |
||||
// this.target.aggregation.crossSeriesReducer = crossSeriesReducer;
|
||||
// this.refresh();
|
||||
} |
||||
|
||||
render() { |
||||
const { labelData, loadLabelsPromise, target } = this.state; |
||||
const { defaultProject, metricType } = target; |
||||
const { templateSrv, datasource, uiSegmentSrv } = this.props; |
||||
|
||||
return ( |
||||
<React.Fragment> |
||||
<MetricPicker |
||||
defaultProject={defaultProject} |
||||
metricType={metricType} |
||||
templateSrv={templateSrv} |
||||
datasource={datasource} |
||||
onChange={value => this.handleMetricTypeChange(value)} |
||||
/> |
||||
<Filter |
||||
onChange={() => console.log('change filter')} |
||||
target={target} |
||||
uiSegmentSrv={uiSegmentSrv} |
||||
labelData={labelData} |
||||
templateSrv={templateSrv} |
||||
loading={loadLabelsPromise} |
||||
/> |
||||
{/* target="ctrl.target" refresh="ctrl.refresh()" loading="ctrl.loadLabelsPromise" label-data="ctrl.labelData" */} |
||||
{/* <stackdriver-filter |
||||
target="target" |
||||
refresh="target.refresh()" |
||||
loading="target.loadLabelsPromise" |
||||
label-data="target.labelData" |
||||
/> |
||||
<aggregation-picker |
||||
value-type="target.target.valueType" |
||||
metric-kind="target.target.metricKind" |
||||
aggregation="target.target.aggregation" |
||||
alignment-period="target.lastQueryMeta.alignmentPeriod" |
||||
refresh="target.refresh()" |
||||
template-srv="target.templateSrv" |
||||
datasource="target.datasource" |
||||
on-change="target.handleAggregationChange" |
||||
/> |
||||
|
||||
<stackdriver-aggregation |
||||
target="target.target" |
||||
alignment-period="target.lastQueryMeta.alignmentPeriod" |
||||
refresh="target.refresh()" |
||||
/> */} |
||||
</React.Fragment> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; |
||||
import 'app/core/directives/metric_segment'; |
||||
|
||||
interface QueryEditorProps { |
||||
segment: any; |
||||
getOptions: () => Promise<any[]>; |
||||
onChange: (segment, index) => void; |
||||
key: number; |
||||
} |
||||
|
||||
export default class Segment extends PureComponent<QueryEditorProps, any> { |
||||
element: any; |
||||
component: AngularComponent; |
||||
|
||||
async componentDidMount() { |
||||
if (!this.element) { |
||||
return; |
||||
} |
||||
|
||||
const { segment, getOptions, onChange } = this.props; |
||||
const loader = getAngularLoader(); |
||||
const template = '<metric-segment> </metric-segment>'; |
||||
|
||||
const scopeProps = { |
||||
segment, |
||||
onChange, |
||||
getOptions, |
||||
debounce: false, |
||||
}; |
||||
|
||||
this.component = loader.load(this.element, scopeProps, template); |
||||
} |
||||
|
||||
componentWillUnmount() { |
||||
if (this.component) { |
||||
this.component.destroy(); |
||||
} |
||||
} |
||||
|
||||
render() { |
||||
return <div ref={element => (this.element = element)} style={{ width: '100%' }} />; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue