Stackdriver: Support meta labels (#21373)

* Rewrite angular segments for filter and group by in react

* wip: refactoring

* Update metric find queries

* Remove old maps used to create labels - use one map for all types instead

* Use value as label (again) for filters ang groupby

* Remove old filter

* Remove not used code

* Fixes after pr feedback

* Fix broken tests and add new metadata tests

* Add index file to make imports cleaner

* Cleanup. Remove old angular filter code

* Fix broken tests

* Use type switching instead of if statements

* Use globals for regex

* Updates after pr feedback

* Make sure it's possible to filter using the same key multiple times

* Replace metric select with segment component

* Pass template vars as props

* Refactor meta labels code

* Reorder template variables

* Fix broken tests

* Reset metric value when changing service

* Fix lint issue.

* Make tests independant of element order

* Include kubernetes.io in regex

* Add instruction in help section
This commit is contained in:
Erik Sundell
2020-01-17 12:25:47 +01:00
committed by GitHub
parent 72023d90bd
commit 260239d98b
33 changed files with 792 additions and 1597 deletions

View File

@@ -16,6 +16,7 @@ const props: Props = {
crossSeriesReducer: '',
groupBys: [],
children: renderProps => <div />,
templateVariableOptions: [],
};
describe('Aggregations', () => {
@@ -32,7 +33,7 @@ describe('Aggregations', () => {
wrapper = shallow(<Aggregations {...newProps} />);
});
it('', () => {
const options = wrapper.state().aggOptions[0].options;
const options = wrapper.state().aggOptions;
expect(options.length).toEqual(11);
expect(options.map((o: any) => o.value)).toEqual(
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
@@ -49,8 +50,7 @@ describe('Aggregations', () => {
wrapper = shallow(<Aggregations {...newProps} />);
});
it('', () => {
const options = wrapper.state().aggOptions[0].options;
const options = wrapper.state().aggOptions;
expect(options.length).toEqual(10);
expect(options.map((o: any) => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE']));
});

View File

@@ -1,14 +1,13 @@
import React from 'react';
import _ from 'lodash';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { ValueTypes, MetricKind } from '../constants';
export interface Props {
onChange: (metricDescriptor: any) => void;
templateSrv: TemplateSrv;
metricDescriptor: {
valueType: string;
metricKind: string;
@@ -16,6 +15,7 @@ export interface Props {
crossSeriesReducer: string;
groupBys: string[];
children?: (renderProps: any) => JSX.Element;
templateVariableOptions: Array<SelectableValue<string>>;
}
export interface State {
@@ -40,19 +40,13 @@ export class Aggregations extends React.Component<Props, State> {
setAggOptions({ metricDescriptor }: Props) {
let aggOptions: any[] = [];
if (metricDescriptor) {
aggOptions = [
{
label: 'Aggregations',
expanded: true,
options: getAggregationOptionsByMetric(
metricDescriptor.valueType as ValueTypes,
metricDescriptor.metricKind as MetricKind
).map(a => ({
...a,
label: a.text,
})),
},
];
aggOptions = getAggregationOptionsByMetric(
metricDescriptor.valueType as ValueTypes,
metricDescriptor.metricKind as MetricKind
).map(a => ({
...a,
label: a.text,
}));
}
this.setState({ aggOptions });
}
@@ -65,22 +59,28 @@ export class Aggregations extends React.Component<Props, State> {
render() {
const { displayAdvancedOptions, aggOptions } = this.state;
const { templateSrv, onChange, crossSeriesReducer } = this.props;
const { templateVariableOptions, onChange, crossSeriesReducer } = this.props;
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<label className="gf-form-label query-keyword width-9">Aggregation</label>
<MetricSelect
onChange={onChange}
value={crossSeriesReducer}
variables={templateSrv.variables}
options={aggOptions}
placeholder="Select Reducer"
className="width-15"
/>
</div>
<label className="gf-form-label query-keyword width-9">Aggregation</label>
<Segment
onChange={({ value }) => onChange(value)}
value={[...aggOptions, ...templateVariableOptions].find(s => s.value === crossSeriesReducer)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
></Segment>
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow">
<a onClick={this.onToggleDisplayAdvanced}>

View File

@@ -1,14 +1,16 @@
import React, { FC } from 'react';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { alignmentPeriods, alignOptions } from '../constants';
import { TemplateSrv } from 'app/features/templating/template_srv';
import kbn from 'app/core/utils/kbn';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { alignmentPeriods, alignOptions } from '../constants';
export interface Props {
onChange: (alignmentPeriod: any) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
alignmentPeriod: string;
perSeriesAligner: string;
usedAlignmentPeriod: string;
@@ -17,36 +19,38 @@ export interface Props {
export const AlignmentPeriods: FC<Props> = ({
alignmentPeriod,
templateSrv,
templateVariableOptions,
onChange,
perSeriesAligner,
usedAlignmentPeriod,
}) => {
const alignment = alignOptions.find(ap => ap.value === templateSrv.replace(perSeriesAligner));
const formatAlignmentText = `${kbn.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`;
const options = alignmentPeriods.map(ap => ({
...ap,
label: ap.text,
}));
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<MetricSelect
onChange={onChange}
value={alignmentPeriod}
variables={templateSrv.variables}
options={[
{
label: 'Alignment options',
expanded: true,
options: alignmentPeriods.map(ap => ({
...ap,
label: ap.text,
})),
},
]}
placeholder="Select Alignment"
className="width-15"
/>
</div>
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<Segment
onChange={({ value }) => onChange(value)}
value={[...options, ...templateVariableOptions].find(s => s.value === alignmentPeriod)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: options,
},
]}
placeholder="Select Alignment"
></Segment>
<div className="gf-form gf-form--grow">
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
</div>

View File

@@ -1,30 +1,37 @@
import React, { FC } from 'react';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
export interface Props {
onChange: (perSeriesAligner: any) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
alignOptions: Array<SelectableValue<string>>;
perSeriesAligner: string;
}
export const Alignments: FC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => {
return (
<>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form offset-width-9">
<label className="gf-form-label query-keyword width-15">Aligner</label>
<MetricSelect
onChange={onChange}
value={perSeriesAligner}
variables={templateSrv.variables}
options={alignOptions}
<Segment
onChange={({ value }) => onChange(value)}
value={[...alignOptions, ...templateVariableOptions].find(s => s.value === perSeriesAligner)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Alignment options',
expanded: true,
options: alignOptions,
},
]}
placeholder="Select Alignment"
className="width-15"
/>
></Segment>
</div>
</div>
</>

View File

@@ -2,12 +2,12 @@ import React from 'react';
import { Input } from '@grafana/ui';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from '../datasource';
import { Metrics } from './Metrics';
import { Filter } from './Filter';
import { Metrics, Filters, AnnotationsHelp } from './';
import { toOption } from '../functions';
import { AnnotationTarget, MetricDescriptor } from '../types';
import { AnnotationsHelp } from './AnnotationsHelp';
export interface Props {
onQueryChange: (target: AnnotationTarget) => void;
@@ -17,6 +17,9 @@ export interface Props {
}
interface State extends AnnotationTarget {
variableOptionGroup: SelectableValue<string>;
variableOptions: Array<SelectableValue<string>>;
labels: any;
[key: string]: any;
}
@@ -29,19 +32,32 @@ const DefaultTarget: State = {
refId: 'annotationQuery',
title: '',
text: '',
labels: {},
variableOptionGroup: {},
variableOptions: [],
};
export class AnnotationQueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
componentDidMount() {
const { target, datasource } = this.props;
const variableOptionGroup = {
label: 'Template Variables',
options: datasource.variables.map(toOption),
};
this.setState({
...this.props.target,
variableOptionGroup,
variableOptions: variableOptionGroup.options,
...target,
});
datasource.getLabels(target.metricType, target.refId).then(labels => this.setState({ labels }));
}
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { onQueryChange } = this.props;
const { onQueryChange, datasource } = this.props;
this.setState(
{
metricType: type,
@@ -53,6 +69,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
onQueryChange(this.state);
}
);
datasource.getLabels(type, this.state.refId).then(labels => this.setState({ labels }));
};
onChange(prop: string, value: string | string[]) {
@@ -62,28 +79,35 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
}
render() {
const { defaultProject, metricType, filters, refId, title, text } = this.state;
const { datasource, templateSrv } = this.props;
const {
defaultProject,
metricType,
filters,
title,
text,
variableOptionGroup,
labels,
variableOptions,
} = this.state;
const { datasource } = this.props;
return (
<>
<Metrics
defaultProject={defaultProject}
metricType={metricType}
templateSrv={templateSrv}
templateSrv={datasource.templateSrv}
datasource={datasource}
onChange={this.onMetricTypeChange}
templateVariableOptions={variableOptions}
onChange={metric => this.onMetricTypeChange(metric)}
>
{metric => (
<>
<Filter
filtersChanged={value => this.onChange('filters', value)}
<Filters
labels={labels}
filters={filters}
refId={refId}
hideGroupBys={true}
templateSrv={templateSrv}
datasource={datasource}
metricType={metric ? metric.type : ''}
onChange={value => this.onChange('filters', value)}
variableOptionGroup={variableOptionGroup}
/>
</>
)}

View File

@@ -1,116 +0,0 @@
import React from 'react';
import _ from 'lodash';
import appEvents from 'app/core/app_events';
import { QueryMeta } from '../types';
import { getAngularLoader, AngularComponent } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import StackdriverDatasource from '../datasource';
import '../query_filter_ctrl';
import { AppEvents } from '@grafana/data';
export interface Props {
filtersChanged: (filters: string[]) => void;
groupBysChanged?: (groupBys: string[]) => void;
metricType: string;
templateSrv: TemplateSrv;
groupBys?: string[];
filters: string[];
datasource: StackdriverDatasource;
refId: string;
hideGroupBys: boolean;
}
interface State {
labelData: QueryMeta;
loading: Promise<any>;
}
const labelData: any = {
metricLabels: {},
resourceLabels: {},
resourceTypes: [],
};
export class Filter extends React.Component<Props, State> {
element: any;
component: AngularComponent;
async componentDidMount() {
if (!this.element) {
return;
}
const { groupBys, filters, hideGroupBys } = this.props;
const loader = getAngularLoader();
const filtersChanged = (filters: string[]) => {
this.props.filtersChanged(filters);
};
const groupBysChanged = (groupBys: string[]) => {
this.props.groupBysChanged(groupBys);
};
const scopeProps: any = {
loading: null,
labelData,
groupBys,
filters,
filtersChanged,
groupBysChanged,
hideGroupBys,
};
const loading = this.loadLabels(scopeProps);
scopeProps.loading = loading;
const template = `<stackdriver-filter
filters="filters"
group-bys="groupBys"
label-data="labelData"
loading="loading"
filters-changed="filtersChanged(filters)"
group-bys-changed="groupBysChanged(groupBys)"
hide-group-bys="hideGroupBys"/>`;
this.component = loader.load(this.element, scopeProps, template);
}
componentDidUpdate(prevProps: Props) {
if (!this.element) {
return;
}
const scope = this.component.getScope();
if (prevProps.metricType !== this.props.metricType) {
scope.loading = this.loadLabels(scope);
}
scope.filters = this.props.filters;
scope.groupBys = this.props.groupBys;
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();
}
}
async loadLabels(scope: any) {
return new Promise(async resolve => {
try {
if (!this.props.metricType) {
scope.labelData = labelData;
} else {
const { meta } = await this.props.datasource.getLabels(this.props.metricType, this.props.refId);
scope.labelData = meta;
}
resolve();
} catch (error) {
appEvents.emit(AppEvents.alertError, ['Error', 'Error loading metric labels for ' + this.props.metricType]);
scope.labelData = labelData;
resolve();
}
});
}
render() {
return <div ref={element => (this.element = element)} style={{ width: '100%' }} />;
}
}

View File

@@ -0,0 +1,103 @@
import React, { FunctionComponent, Fragment } from 'react';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { labelsToGroupedOptions, toOption } from '../functions';
import { Filter } from '../types';
export interface Props {
labels: { [key: string]: string[] };
filters: string[];
onChange: (filters: string[]) => void;
variableOptionGroup: SelectableValue<string>;
}
const removeText = '-- remove filter --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'fa fa-remove' };
const operators = ['=', '!=', '=~', '!=~'];
const filtersToStringArray = (filters: Filter[]) =>
_.flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition]));
const stringArrayToFilters = (filterArray: string[]) =>
_.chunk(filterArray, 4).map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}));
export const Filters: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
variableOptionGroup,
}) => {
const filters = stringArrayToFilters(filterArray);
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))];
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Filter</label>
{filters.map(({ key, operator, value, condition }, index) => (
<Fragment key={index}>
<Segment
allowCustomValue
value={key}
options={options}
onChange={({ value: key }) => {
if (key === removeText) {
onChange(filtersToStringArray(filters.filter((_, i) => i !== index)));
} else {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}
}}
/>
<Segment
value={operator}
className="gf-form-label query-segment-operator"
options={operators.map(toOption)}
onChange={({ value: operator }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
/>
<Segment
allowCustomValue
value={value}
placeholder="add filter value"
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
{filters.length > 1 && index + 1 !== filters.length && (
<label className="gf-form-label query-keyword">{condition}</label>
)}
</Fragment>
))}
{Object.values(filters).every(({ value }) => value) && (
<Segment
allowCustomValue
Component={
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
}
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' } as Filter]))
}
/>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};

View File

@@ -0,0 +1,58 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { labelsToGroupedOptions } from '../functions';
import { systemLabels } from '../constants';
export interface Props {
values: string[];
onChange: (values: string[]) => void;
variableOptionGroup: SelectableValue<string>;
groupBys: string[];
}
const removeText = '-- remove group by --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [], onChange, variableOptionGroup }) => {
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...systemLabels])];
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Group By</label>
{values &&
values.map((value, index) => (
<Segment
allowCustomValue
key={value + index}
value={value}
options={options}
onChange={({ value }) =>
onChange(
value === removeText
? values.filter((_, i) => i !== index)
: values.map((v, i) => (i === index ? value : v))
)
}
/>
))}
{values.length !== groupBys.length && (
<Segment
Component={
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
}
allowCustomValue
onChange={({ value }) => onChange([...values, value])}
options={[
variableOptionGroup,
...labelsToGroupedOptions([...groupBys.filter(groupBy => !values.includes(groupBy)), ...systemLabels]),
]}
/>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};

View File

@@ -97,6 +97,14 @@ export class Help extends React.Component<Props, State> {
<li>
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
</li>
<li>
<code>{`${'{{metadata.system_labels.name}}'}`}</code> = Meta data system labels e.g.
metadata.system_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<code>{`${'{{metadata.user_labels.name}}'}`}</code> = Meta data user labels e.g.
metadata.user_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
Grafana

View File

@@ -1,16 +1,18 @@
import React from 'react';
import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from '../datasource';
import appEvents from 'app/core/app_events';
import { Segment } from '@grafana/ui';
import { MetricDescriptor } from '../types';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CoreEvents } from 'app/types';
export interface Props {
onChange: (metricDescriptor: MetricDescriptor) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: StackdriverDatasource;
defaultProject: string;
metricType: string;
@@ -104,9 +106,9 @@ export class Metrics extends React.Component<Props, State> {
return metricsByService;
}
onServiceChange = (service: any) => {
onServiceChange = ({ value: service }: any) => {
const { metricDescriptors } = this.state;
const { templateSrv, metricType } = this.props;
const { metricType, templateSrv } = this.props;
const metrics = metricDescriptors
.filter(m => m.service === templateSrv.replace(service))
@@ -120,11 +122,11 @@ export class Metrics extends React.Component<Props, State> {
this.setState({ service, metrics });
if (metrics.length > 0 && !metrics.some(m => m.value === templateSrv.replace(metricType))) {
this.onMetricTypeChange(metrics[0].value);
this.onMetricTypeChange(metrics[0]);
}
};
onMetricTypeChange = (value: any) => {
onMetricTypeChange = ({ value }: any) => {
const metricDescriptor = this.getSelectedMetricDescriptor(value);
this.setState({ metricDescriptor });
this.props.onChange({ ...metricDescriptor, type: value });
@@ -139,55 +141,46 @@ export class Metrics extends React.Component<Props, State> {
return services.length > 0 ? _.uniqBy(services, s => s.value) : [];
}
getTemplateVariablesGroup() {
return {
label: 'Template Variables',
options: this.props.templateSrv.variables.map(v => ({
label: `$${v.name}`,
value: `$${v.name}`,
})),
};
}
render() {
const { services, service, metrics } = this.state;
const { metricType, templateSrv } = this.props;
const { metricType, templateVariableOptions } = this.props;
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-9 query-keyword">Service</span>
<MetricSelect
onChange={this.onServiceChange}
value={service}
options={services}
placeholder="Select Services"
className="width-15"
/>
</div>
<span className="gf-form-label width-9 query-keyword">Service</span>
<Segment
onChange={this.onServiceChange}
value={[...services, ...templateVariableOptions].find(s => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
></Segment>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-9 query-keyword">Metric</span>
<MetricSelect
onChange={this.onMetricTypeChange}
value={metricType}
variables={templateSrv.variables}
options={[
{
label: 'Metrics',
expanded: true,
options: metrics,
},
]}
placeholder="Select Metric"
className="width-26"
/>
</div>
<span className="gf-form-label width-9 query-keyword">Metric</span>
<Segment
className="query-part"
onChange={this.onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find(s => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
></Segment>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>

View File

@@ -11,6 +11,8 @@ const props: Props = {
datasource: {
getDefaultProject: () => Promise.resolve('project'),
getMetricTypes: () => Promise.resolve([]),
getLabels: () => Promise.resolve([]),
variables: [],
} as any,
templateSrv: new TemplateSrv(),
};

View File

@@ -3,15 +3,9 @@ import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { Metrics } from './Metrics';
import { Filter } from './Filter';
import { Aggregations } from './Aggregations';
import { Alignments } from './Alignments';
import { AlignmentPeriods } from './AlignmentPeriods';
import { AliasBy } from './AliasBy';
import { Help } from './Help';
import { Aggregations, Metrics, Filters, GroupBys, Alignments, AlignmentPeriods, AliasBy, Help } from './';
import { StackdriverQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import { getAlignmentPickerData, toOption } from '../functions';
import StackdriverDatasource from '../datasource';
import { TimeSeries, SelectableValue } from '@grafana/data';
import { PanelEvents } from '@grafana/data';
@@ -26,9 +20,12 @@ export interface Props {
}
interface State extends StackdriverQuery {
variableOptions: Array<SelectableValue<string>>;
variableOptionGroup: SelectableValue<string>;
alignOptions: Array<SelectableValue<string>>;
lastQuery: string;
lastQueryError: string;
labels: any;
[key: string]: any;
}
@@ -45,26 +42,39 @@ export const DefaultTarget: State = {
perSeriesAligner: 'ALIGN_MEAN',
groupBys: [],
filters: [],
filter: [],
aliasBy: '',
alignOptions: [],
lastQuery: '',
lastQueryError: '',
usedAlignmentPeriod: '',
labels: {},
variableOptionGroup: {},
variableOptions: [],
};
export class QueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
componentDidMount() {
const { events, target, templateSrv } = this.props;
async componentDidMount() {
const { events, target, templateSrv, datasource } = this.props;
events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
events.on(PanelEvents.dataError, this.onDataError.bind(this));
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
const variableOptionGroup = {
label: 'Template Variables',
expanded: false,
options: datasource.variables.map(toOption),
};
this.setState({
...this.props.target,
alignOptions,
perSeriesAligner,
variableOptionGroup,
variableOptions: variableOptionGroup.options,
});
datasource.getLabels(target.metricType, target.refId, target.groupBys).then(labels => this.setState({ labels }));
}
componentWillUnmount() {
@@ -102,12 +112,13 @@ export class QueryEditor extends React.Component<Props, State> {
this.setState({ lastQuery, lastQueryError });
}
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { templateSrv, onQueryChange, onExecuteQuery } = this.props;
onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { templateSrv, onQueryChange, onExecuteQuery, target } = this.props;
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
{ valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
templateSrv
);
const labels = await this.props.datasource.getLabels(type, target.refId, target.groupBys);
this.setState(
{
alignOptions,
@@ -116,6 +127,7 @@ export class QueryEditor extends React.Component<Props, State> {
unit,
valueType,
metricKind,
labels,
},
() => {
onQueryChange(this.state);
@@ -124,6 +136,15 @@ export class QueryEditor extends React.Component<Props, State> {
);
};
onGroupBysChange(value: string[]) {
const { target, datasource } = this.props;
this.setState({ groupBys: value }, () => {
this.props.onQueryChange(this.state);
this.props.onExecuteQuery();
});
datasource.getLabels(target.metricType, target.refId, value).then(labels => this.setState({ labels }));
}
onPropertyChange(prop: string, value: string[]) {
this.setState({ [prop]: value }, () => {
this.props.onQueryChange(this.state);
@@ -145,35 +166,39 @@ export class QueryEditor extends React.Component<Props, State> {
aliasBy,
lastQuery,
lastQueryError,
refId,
labels,
variableOptionGroup,
variableOptions,
} = this.state;
const { datasource, templateSrv } = this.props;
return (
<>
<Metrics
templateSrv={templateSrv}
defaultProject={defaultProject}
metricType={metricType}
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
datasource={datasource}
onChange={this.onMetricTypeChange}
>
{metric => (
<>
<Filter
filtersChanged={value => this.onPropertyChange('filters', value)}
groupBysChanged={value => this.onPropertyChange('groupBys', value)}
<Filters
labels={labels}
filters={filters}
groupBys={groupBys}
refId={refId}
hideGroupBys={false}
templateSrv={templateSrv}
datasource={datasource}
metricType={metric ? metric.type : ''}
onChange={value => this.onPropertyChange('filters', value)}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={groupBys}
onChange={this.onGroupBysChange.bind(this)}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
crossSeriesReducer={crossSeriesReducer}
groupBys={groupBys}
onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
@@ -182,7 +207,7 @@ export class QueryEditor extends React.Component<Props, State> {
displayAdvancedOptions && (
<Alignments
alignOptions={alignOptions}
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
perSeriesAligner={perSeriesAligner}
onChange={value => this.onPropertyChange('perSeriesAligner', value)}
/>
@@ -191,6 +216,7 @@ export class QueryEditor extends React.Component<Props, State> {
</Aggregations>
<AlignmentPeriods
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
alignmentPeriod={alignmentPeriod}
perSeriesAligner={perSeriesAligner}
usedAlignmentPeriod={usedAlignmentPeriod}

View File

@@ -7,7 +7,7 @@ interface Props {
label: string;
}
const SimpleSelect: FC<Props> = props => {
export const SimpleSelect: FC<Props> = props => {
const { label, onValueChange, value, options } = props;
return (
<div className="gf-form max-width-21">
@@ -24,5 +24,3 @@ const SimpleSelect: FC<Props> = props => {
</div>
);
};
export default SimpleSelect;

View File

@@ -1,6 +1,6 @@
import React, { ChangeEvent, PureComponent } from 'react';
import { VariableQueryProps } from 'app/types/plugins';
import SimpleSelect from './SimpleSelect';
import { SimpleSelect } from './';
import { getMetricTypes, getLabelKeys, extractServicesFromMetricDescriptors } from '../functions';
import { MetricFindQueryTypes, VariableQueryData } from '../types';

View File

@@ -5,99 +5,20 @@ Array [
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Aggregation
</label>
<div
className="gf-form"
onClick={[Function]}
>
<label
className="gf-form-label query-keyword width-9"
<a
className="gf-form-label query-part query-placeholder"
>
Aggregation
</label>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Reducer
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-2-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Reducer
</a>
</div>
<div
className="gf-form gf-form--grow"

View File

@@ -5,99 +5,20 @@ Array [
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Service
</span>
<div
className="gf-form"
onClick={[Function]}
>
<span
className="gf-form-label width-9 query-keyword"
<a
className="gf-form-label query-part query-placeholder"
>
Service
</span>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Services
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-2-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Services
</a>
</div>
<div
className="gf-form gf-form--grow"
@@ -110,99 +31,20 @@ Array [
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Metric
</span>
<div
className="gf-form"
onClick={[Function]}
>
<span
className="gf-form-label width-9 query-keyword"
<a
className="gf-form-label query-part query-placeholder query-part"
>
Metric
</span>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Metric
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-3-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Metric
</a>
</div>
<div
className="gf-form gf-form--grow"
@@ -212,109 +54,67 @@ Array [
/>
</div>
</div>,
<div
style={
Object {
"width": "100%",
}
}
/>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Filter
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label query-keyword width-9"
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Group By
</label>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Aggregation
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Aggregation
</label>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Reducer
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-4-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Reducer
</a>
</div>
<div
className="gf-form gf-form--grow"
@@ -336,103 +136,20 @@ Array [
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Alignment Period
</label>
<div
className="gf-form"
onClick={[Function]}
>
<label
className="gf-form-label query-keyword width-9"
<a
className="gf-form-label query-part"
>
Alignment Period
</label>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
>
<div
className="css-0 gf-form-select-box__single-value"
>
<div
className="css-38iae9-singleValue"
>
stackdriver auto
</div>
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-5-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
stackdriver auto
</a>
</div>
<div
className="gf-form gf-form--grow"

View File

@@ -0,0 +1,11 @@
export { Project } from './Project';
export { Metrics } from './Metrics';
export { Help } from './Help';
export { GroupBys } from './GroupBys';
export { Filters } from './Filters';
export { AnnotationsHelp } from './AnnotationsHelp';
export { Alignments } from './Alignments';
export { AlignmentPeriods } from './AlignmentPeriods';
export { AliasBy } from './AliasBy';
export { Aggregations } from './Aggregations';
export { SimpleSelect } from './SimpleSelect';