CloudMonitoring: Add support for preprocessing (#33011)

* add support for handling preprocessors in the backend

* add preprocessor tests

* use uppercase for constants

* add super label component

* remove error message from query editor since its not working (probably cause onDataError doesnt work anymore)

* use cheat sheet instead of help

* add return type annotation for projects

* add support for preprocessing. replace segment comp with select. change components names and refactoring

* cleanup

* more pr feedback

* fix annotations editor

* rename aggregation component

* fix broken test

* remove unnecessary cast

* fix strict errors

* fix more strict errors

* remove not used prop

* update docs

* use same inline label for annotation editor

* fix react prop warning

* disable preprocessing for distribution types

* using new default values for reducer

* auto select 'rate' if metric kind is not gauge

* fix create label format

* pr feedback

* more pr feedback

* update images
This commit is contained in:
Erik Sundell
2021-05-19 08:16:05 +02:00
committed by GitHub
parent e3188458d5
commit 5042dc3b52
45 changed files with 1385 additions and 923 deletions

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { Segment } from '@grafana/ui';
import { Aggregations, Props } from './Aggregations';
import { ValueTypes, MetricKind } from '../constants';
import { Select } from '@grafana/ui';
import { Aggregation, Props } from './Aggregation';
import { ValueTypes, MetricKind } from '../types';
import { TemplateSrvStub } from 'test/specs/helpers';
const props: Props = {
@@ -16,16 +16,13 @@ const props: Props = {
} as any,
crossSeriesReducer: '',
groupBys: [],
children(renderProps) {
return <div />;
},
templateVariableOptions: [],
};
describe('Aggregations', () => {
describe('Aggregation', () => {
it('renders correctly', () => {
render(<Aggregations {...props} />);
expect(screen.getByTestId('aggregations')).toBeInTheDocument();
render(<Aggregation {...props} />);
expect(screen.getByTestId('cloud-monitoring-aggregation')).toBeInTheDocument();
});
describe('options', () => {
@@ -39,8 +36,8 @@ describe('Aggregations', () => {
};
it('should not have the reduce values', () => {
const wrapper = shallow(<Aggregations {...nextProps} />);
const { options } = wrapper.find(Segment).props() as any;
const wrapper = shallow(<Aggregation {...nextProps} />);
const { options } = wrapper.find(Select).props() as any;
const [, aggGroup] = options;
expect(aggGroup.options.length).toEqual(11);
@@ -60,8 +57,8 @@ describe('Aggregations', () => {
};
it('should have the reduce values', () => {
const wrapper = shallow(<Aggregations {...nextProps} />);
const { options } = wrapper.find(Segment).props() as any;
const wrapper = shallow(<Aggregation {...nextProps} />);
const { options } = wrapper.find(Select).props() as any;
const [, aggGroup] = options;
expect(aggGroup.options.length).toEqual(11);

View File

@@ -0,0 +1,65 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { QueryEditorField } from '.';
import { getAggregationOptionsByMetric } from '../functions';
import { MetricDescriptor, ValueTypes, MetricKind } from '../types';
export interface Props {
onChange: (metricDescriptor: string) => void;
metricDescriptor?: MetricDescriptor;
crossSeriesReducer: string;
groupBys: string[];
templateVariableOptions: Array<SelectableValue<string>>;
}
export const Aggregation: FC<Props> = (props) => {
const aggOptions = useAggregationOptionsByMetric(props);
const selected = useSelectedFromOptions(aggOptions, props);
return (
<QueryEditorField labelWidth={18} label="Group by function" data-testid="cloud-monitoring-aggregation">
<Select
width={16}
onChange={({ value }) => props.onChange(value!)}
value={selected}
options={[
{
label: 'Template Variables',
options: props.templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
/>
</QueryEditorField>
);
};
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
const valueType = metricDescriptor?.valueType;
const metricKind = metricDescriptor?.metricKind;
return useMemo(() => {
if (!valueType || !metricKind) {
return [];
}
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
...a,
label: a.text,
}));
}, [valueType, metricKind]);
};
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
return useMemo(() => {
const allOptions = [...aggOptions, ...props.templateVariableOptions];
return allOptions.find((s) => s.value === props.crossSeriesReducer);
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
};

View File

@@ -1,79 +0,0 @@
import React, { FC, useState, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment, Icon } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { ValueTypes, MetricKind } from '../constants';
import { MetricDescriptor } from '../types';
export interface Props {
onChange: (metricDescriptor: string) => void;
metricDescriptor?: MetricDescriptor;
crossSeriesReducer: string;
groupBys: string[];
children: (displayAdvancedOptions: boolean) => React.ReactNode;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const Aggregations: FC<Props> = (props) => {
const [displayAdvancedOptions, setDisplayAdvancedOptions] = useState(false);
const aggOptions = useAggregationOptionsByMetric(props);
const selected = useSelectedFromOptions(aggOptions, props);
return (
<div data-testid="aggregations">
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Aggregation</label>
<Segment
onChange={({ value }) => props.onChange(value!)}
value={selected}
options={[
{
label: 'Template Variables',
options: props.templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
/>
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow">
<a onClick={() => setDisplayAdvancedOptions(!displayAdvancedOptions)}>
<>
<Icon name={displayAdvancedOptions ? 'angle-down' : 'angle-right'} /> Advanced Options
</>
</a>
</label>
</div>
</div>
{props.children(displayAdvancedOptions)}
</div>
);
};
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
const valueType = metricDescriptor?.valueType;
const metricKind = metricDescriptor?.metricKind;
return useMemo(() => {
if (!valueType || !metricKind) {
return [];
}
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
...a,
label: a.text,
}));
}, [valueType, metricKind]);
};
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
return useMemo(() => {
const allOptions = [...aggOptions, ...props.templateVariableOptions];
return allOptions.find((s) => s.value === props.crossSeriesReducer);
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
};

View File

@@ -1,6 +1,8 @@
import React, { FunctionComponent, useState } from 'react';
import { debounce } from 'lodash';
import { QueryInlineField } from '.';
import { Input } from '@grafana/ui';
import { QueryEditorRow } from '.';
import { INPUT_WIDTH } from '../constants';
export interface Props {
onChange: (alias: any) => void;
@@ -18,8 +20,8 @@ export const AliasBy: FunctionComponent<Props> = ({ value = '', onChange }) => {
};
return (
<QueryInlineField label="Alias By">
<input type="text" className="gf-form-input width-26" value={alias} onChange={onChange} />
</QueryInlineField>
<QueryEditorRow label="Alias by">
<Input width={INPUT_WIDTH} value={alias} onChange={onChange} />
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,34 @@
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
import { SELECT_WIDTH } from '../constants';
import { CustomMetaData, MetricQuery } from '../types';
import { AlignmentFunction, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.';
import CloudMonitoringDatasource from '../datasource';
export interface Props {
onChange: (query: MetricQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const Alignment: FC<Props> = ({ templateVariableOptions, onChange, query, customMetaData, datasource }) => {
return (
<QueryEditorRow
label="Alignment function"
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
fillComponent={<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />}
>
<AlignmentFunction templateVariableOptions={templateVariableOptions} query={query} onChange={onChange} />
<QueryEditorField label="Alignment period">
<AlignmentPeriod
selectWidth={SELECT_WIDTH}
templateVariableOptions={templateVariableOptions}
query={query}
onChange={onChange}
/>
</QueryEditorField>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,40 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { MetricQuery } from '../types';
import { getAlignmentPickerData } from '../functions';
import { SELECT_WIDTH } from '../constants';
export interface Props {
onChange: (query: MetricQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const AlignmentFunction: FC<Props> = ({ query, templateVariableOptions, onChange }) => {
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
const { perSeriesAligner, alignOptions } = useMemo(
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
[valueType, metricKind, psa, preprocessor]
);
return (
<Select
width={SELECT_WIDTH}
onChange={({ value }) => onChange({ ...query, perSeriesAligner: 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"
></Select>
);
};

View File

@@ -0,0 +1,44 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { ALIGNMENT_PERIODS } from '../constants';
import { BaseQuery } from '../types';
export interface Props {
onChange: (query: BaseQuery) => void;
query: BaseQuery;
templateVariableOptions: Array<SelectableValue<string>>;
selectWidth?: number;
}
export const AlignmentPeriod: FC<Props> = ({ templateVariableOptions, onChange, query, selectWidth }) => {
const options = useMemo(
() =>
ALIGNMENT_PERIODS.map((ap) => ({
...ap,
label: ap.text,
})),
[]
);
const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]);
return (
<Select
width={selectWidth}
onChange={({ value }) => onChange({ ...query, alignmentPeriod: value! })}
value={[...options, ...templateVariableOptions].find((s) => s.value === query.alignmentPeriod)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: visibleOptions,
},
]}
placeholder="Select Alignment"
></Select>
);
};

View File

@@ -0,0 +1,26 @@
import React, { FC, useMemo } from 'react';
import { rangeUtil } from '@grafana/data';
import { ALIGNMENTS } from '../constants';
import CloudMonitoringDatasource from '../datasource';
import { CustomMetaData } from '../types';
export interface Props {
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const AlignmentPeriodLabel: FC<Props> = ({ customMetaData, datasource }) => {
const { perSeriesAligner, alignmentPeriod } = customMetaData;
const formatAlignmentText = useMemo(() => {
if (!alignmentPeriod || !perSeriesAligner) {
return '';
}
const alignment = ALIGNMENTS.find((ap) => ap.value === datasource.templateSrv.replace(perSeriesAligner));
const seconds = parseInt(alignmentPeriod ?? ''.replace(/[^0-9]/g, ''), 10);
const hms = rangeUtil.secondsToHms(seconds);
return `${hms} interval (${alignment?.text ?? ''})`;
}, [datasource, perSeriesAligner, alignmentPeriod]);
return <label>{formatAlignmentText}</label>;
};

View File

@@ -1,61 +0,0 @@
import React, { FC } from 'react';
import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue, rangeUtil } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { alignmentPeriods, alignOptions } from '../constants';
export interface Props {
onChange: (alignmentPeriod: string) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
alignmentPeriod: string;
perSeriesAligner: string;
usedAlignmentPeriod?: number;
}
export const AlignmentPeriods: FC<Props> = ({
alignmentPeriod,
templateSrv,
templateVariableOptions,
onChange,
perSeriesAligner,
usedAlignmentPeriod,
}) => {
const alignment = alignOptions.find((ap) => ap.value === templateSrv.replace(perSeriesAligner));
const formatAlignmentText = usedAlignmentPeriod
? `${rangeUtil.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`
: '';
const options = alignmentPeriods.map((ap) => ({
...ap,
label: ap.text,
}));
const visibleOptions = options.filter((ap) => !ap.hidden);
return (
<>
<div className="gf-form-inline">
<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: visibleOptions,
},
]}
placeholder="Select Alignment"
></Segment>
<div className="gf-form gf-form--grow">
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
</div>
</div>
</>
);
};

View File

@@ -1,39 +0,0 @@
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
export interface Props {
onChange: (perSeriesAligner: string) => void;
templateVariableOptions: Array<SelectableValue<string>>;
alignOptions: Array<SelectableValue<string>>;
perSeriesAligner: string;
}
export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => {
return (
<>
<div className="gf-form-inline">
<div className="gf-form offset-width-9">
<label className="gf-form-label query-keyword width-15">Aligner</label>
<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"
></Segment>
</div>
</div>
</>
);
};

View File

@@ -4,9 +4,9 @@ import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue } from '@grafana/data';
import CloudMonitoringDatasource from '../datasource';
import { AnnotationsHelp, LabelFilter, Metrics, Project } from './';
import { AnnotationsHelp, LabelFilter, Metrics, Project, QueryEditorRow } from './';
import { toOption } from '../functions';
import { AnnotationTarget, EditorMode, MetricDescriptor } from '../types';
import { AnnotationTarget, EditorMode, MetricDescriptor, MetricKind } from '../types';
const { Input } = LegacyForms;
@@ -30,7 +30,7 @@ const DefaultTarget: State = {
projects: [],
metricType: '',
filters: [],
metricKind: '',
metricKind: MetricKind.GAUGE,
valueType: '',
refId: 'annotationQuery',
title: '',
@@ -122,29 +122,23 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
</>
)}
</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.onChange('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"
value={text}
onChange={(e) => this.onChange('text', e.target.value)}
/>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
<QueryEditorRow label="Title">
<Input
type="text"
className="gf-form-input width-20"
value={title}
onChange={(e) => this.onChange('title', e.target.value)}
/>
</QueryEditorRow>
<QueryEditorRow label="Text">
<Input
type="text"
className="gf-form-input width-20"
value={text}
onChange={(e) => this.onChange('text', e.target.value)}
/>
</QueryEditorRow>
<AnnotationsHelp />
</>

View File

@@ -0,0 +1,72 @@
import React, { PureComponent } from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
import { css } from '@emotion/css';
export default class CloudMonitoringCheatSheet extends PureComponent<QueryEditorHelpProps, { userExamples: string[] }> {
render() {
return (
<div>
<h2>Cloud Monitoring alias patterns</h2>
<div>
<p>
Format the legend keys any way you want by using alias patterns. Format the legend keys any way you want by
using alias patterns.
</p>
Example:
<code>{`${'{{metric.name}} - {{metric.label.instance_name}}'}`}</code>
<br />
Result: &nbsp;&nbsp;<code>cpu/usage_time - server1-europe-west-1</code>
<br />
<br />
<label>Patterns</label>
<br />
<ul
className={css`
list-style: none;
`}
>
<li>
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
</li>
<li>
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g. metric.label.instance_name
</li>
<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
</li>
<li>
<code>{`${'{{project}}'}`}</code> = The project name that was specified in the query editor
</li>
<li>
<code>{`${'{{service}}'}`}</code> = The service id that was specified in the SLO query editor
</li>
<li>
<code>{`${'{{slo}}'}`}</code> = The SLO id that was specified in the SLO query editor
</li>
<li>
<code>{`${'{{selector}}'}`}</code> = The Selector function that was specified in the SLO query editor
</li>
</ul>
</div>
</div>
);
}
}

View File

@@ -1,32 +1,8 @@
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { InlineFormLabel, Select, InlineField } from '@grafana/ui';
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
tooltip?: string;
children?: React.ReactNode;
}
export const QueryField: FunctionComponent<Partial<Props>> = ({ label, tooltip, children }) => (
<>
<InlineFormLabel width={9} className="query-keyword" tooltip={tooltip}>
{label}
</InlineFormLabel>
{children}
</>
);
export const QueryInlineField: FunctionComponent<Props> = ({ ...props }) => {
return (
<div className={'gf-form-inline'}>
<QueryField {...props} />
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
);
};
import { HorizontalGroup, InlineLabel, PopoverContent, Select, InlineField } from '@grafana/ui';
import { css } from '@emotion/css';
import { INNER_LABEL_WIDTH, LABEL_WIDTH } from '../constants';
interface VariableQueryFieldProps {
onChange: (value: string) => void;
@@ -36,7 +12,7 @@ interface VariableQueryFieldProps {
allowCustomValue?: boolean;
}
export const VariableQueryField: FunctionComponent<VariableQueryFieldProps> = ({
export const VariableQueryField: FC<VariableQueryFieldProps> = ({
label,
onChange,
value,
@@ -55,3 +31,58 @@ export const VariableQueryField: FunctionComponent<VariableQueryFieldProps> = ({
</InlineField>
);
};
export interface Props {
children: React.ReactNode;
tooltip?: PopoverContent;
label?: React.ReactNode;
className?: string;
noFillEnd?: boolean;
labelWidth?: number;
fillComponent?: React.ReactNode;
}
export const QueryEditorRow: FC<Props> = ({
children,
label,
tooltip,
fillComponent,
noFillEnd = false,
labelWidth = LABEL_WIDTH,
...rest
}) => {
return (
<div className="gf-form" {...rest}>
{label && (
<InlineLabel width={labelWidth} tooltip={tooltip}>
{label}
</InlineLabel>
)}
<div
className={css`
margin-right: 4px;
`}
>
<HorizontalGroup spacing="xs" width="auto">
{children}
</HorizontalGroup>
</div>
<div className={'gf-form--grow'}>
{noFillEnd || <div className={'gf-form-label gf-form-label--grow'}>{fillComponent}</div>}
</div>
</div>
);
};
export const QueryEditorField: FC<Props> = ({ children, label, tooltip, labelWidth = INNER_LABEL_WIDTH, ...rest }) => {
return (
<>
{label && (
<InlineLabel width={labelWidth} tooltip={tooltip} {...rest}>
{label}
</InlineLabel>
)}
{children}
</>
);
};

View File

@@ -0,0 +1,52 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { MultiSelect } from '@grafana/ui';
import { labelsToGroupedOptions } from '../functions';
import { SYSTEM_LABELS, INPUT_WIDTH } from '../constants';
import { MetricDescriptor, MetricQuery } from '../types';
import { Aggregation, QueryEditorRow } from '.';
export interface Props {
variableOptionGroup: SelectableValue<string>;
labels: string[];
metricDescriptor?: MetricDescriptor;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
}
export const GroupBy: FunctionComponent<Props> = ({
labels: groupBys = [],
query,
onChange,
variableOptionGroup,
metricDescriptor,
}) => {
const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...SYSTEM_LABELS])], [
groupBys,
variableOptionGroup,
]);
return (
<QueryEditorRow
label="Group by"
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series."
>
<MultiSelect
width={INPUT_WIDTH}
placeholder="Choose label"
options={options}
value={query.groupBys ?? []}
onChange={(options) => {
onChange({ ...query, groupBys: options.map((o) => o.value!) });
}}
></MultiSelect>
<Aggregation
metricDescriptor={metricDescriptor}
templateVariableOptions={variableOptionGroup.options}
crossSeriesReducer={query.crossSeriesReducer}
groupBys={query.groupBys ?? []}
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
></Aggregation>
</QueryEditorRow>
);
};

View File

@@ -1,58 +0,0 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment, Icon } 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">
<Icon name="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

@@ -1,139 +0,0 @@
import React from 'react';
import { MetricDescriptor } from '../types';
import { Icon } from '@grafana/ui';
export interface Props {
rawQuery: string;
lastQueryError?: string;
metricDescriptor?: MetricDescriptor;
}
interface State {
displayHelp: boolean;
displaRawQuery: boolean;
}
export class Help extends React.Component<Props, State> {
state: State = {
displayHelp: false,
displaRawQuery: false,
};
onHelpClicked = () => {
this.setState({ displayHelp: !this.state.displayHelp });
};
onRawQueryClicked = () => {
this.setState({ displaRawQuery: !this.state.displaRawQuery });
};
shouldComponentUpdate(nextProps: Props) {
return nextProps.metricDescriptor !== null;
}
render() {
const { displayHelp, displaRawQuery } = this.state;
const { rawQuery, lastQueryError } = this.props;
return (
<>
<div className="gf-form-inline">
<div className="gf-form" onClick={this.onHelpClicked}>
<label className="gf-form-label query-keyword pointer">
Show Help <Icon name={displayHelp ? 'angle-down' : 'angle-right'} />
</label>
</div>
{rawQuery && (
<div className="gf-form" onClick={this.onRawQueryClicked}>
<label className="gf-form-label query-keyword">
Raw query
<Icon
name={displaRawQuery ? 'angle-down' : 'angle-right'}
ng-show="ctrl.showHelp"
style={{ marginTop: '3px' }}
/>
</label>
</div>
)}
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
{rawQuery && displaRawQuery && (
<div className="gf-form">
<pre className="gf-form-pre">{rawQuery}</pre>
</div>
)}
{displayHelp && (
<div className="gf-form grafana-info-box alert-info">
<div>
<h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns. Format the legend
keys any way you want by using alias patterns.
<br /> <br />
Example:
<code>{`${'{{metric.name}} - {{metric.label.instance_name}}'}`}</code>
<br />
Result: &nbsp;&nbsp;<code>cpu/usage_time - server1-europe-west-1</code>
<br />
<br />
<strong>Patterns</strong>
<br />
<ul>
<li>
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g.
compute.googleapis.com/instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
</li>
<li>
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
</li>
<li>
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g.
metric.label.instance_name
</li>
<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
</li>
<li>
<code>{`${'{{project}}'}`}</code> = The project name that was specified in the query editor
</li>
<li>
<code>{`${'{{service}}'}`}</code> = The service id that was specified in the SLO query editor
</li>
<li>
<code>{`${'{{slo}}'}`}</code> = The SLO id that was specified in the SLO query editor
</li>
<li>
<code>{`${'{{selector}}'}`}</code> = The Selector function that was specified in the SLO query editor
</li>
</ul>
</div>
</div>
)}
{lastQueryError && (
<div className="gf-form">
<pre className="gf-form-pre alert alert-error">{lastQueryError}</pre>
</div>
)}
</>
);
}
}

View File

@@ -1,8 +1,13 @@
import React, { FunctionComponent, Fragment } from 'react';
import React, { FunctionComponent, useCallback, useMemo } from 'react';
import { flatten } from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment, Icon } from '@grafana/ui';
import { labelsToGroupedOptions, filtersToStringArray, stringArrayToFilters, toOption } from '../functions';
import { CustomControlProps } from '@grafana/ui/src/components/Select/types';
import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui';
import { labelsToGroupedOptions, stringArrayToFilters, toOption } from '../functions';
import { Filter } from '../types';
import { SELECT_WIDTH } from '../constants';
import { QueryEditorRow } from '.';
export interface Props {
labels: { [key: string]: string[] };
@@ -11,82 +16,114 @@ export interface Props {
variableOptionGroup: SelectableValue<string>;
}
const removeText = '-- remove filter --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'times' };
const operators = ['=', '!=', '=~', '!=~'];
const FilterButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(
({ value, isOpen, invalid, ...rest }, ref) => {
return <Button ref={ref} {...rest} variant="secondary" icon="plus"></Button>;
}
);
FilterButton.displayName = 'FilterButton';
const OperatorButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(({ value, ...rest }, ref) => {
return (
<Button ref={ref} {...rest} variant="secondary">
<span className="query-segment-operator">{value?.label}</span>
</Button>
);
});
OperatorButton.displayName = 'OperatorButton';
export const LabelFilter: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
variableOptionGroup,
}) => {
const filters = stringArrayToFilters(filterArray);
const filters = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))], [
labels,
variableOptionGroup,
]);
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))];
const filtersToStringArray = useCallback((filters: Filter[]) => {
const strArr = flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition!]));
return strArr.slice(0, strArr.length - 1);
}, []);
const AddFilter = () => {
return (
<Select
allowCustomValue
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key = '' }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' }]))
}
menuPlacement="bottom"
renderControl={FilterButton}
/>
);
};
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 {
<QueryEditorRow
label="Filter"
tooltip={
'To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression.'
}
noFillEnd={filters.length > 1}
>
<VerticalGroup spacing="xs" width="auto">
{filters.map(({ key, operator, value, condition }, index) => (
<HorizontalGroup key={index} spacing="xs" width="auto">
<Select
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use label key: ${v}`}
value={key}
options={options}
onChange={({ value: key = '' }) => {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}}
/>
<Select
value={operator}
options={operators.map(toOption)}
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : 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">
<Icon name="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>
menuPlacement="bottom"
renderControl={OperatorButton}
/>
<Select
width={SELECT_WIDTH}
formatCreateLabel={(v) => `Use label value: ${v}`}
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))))
}
/>
<Button
variant="secondary"
size="md"
icon="trash-alt"
aria-label="Remove"
onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))}
></Button>
{index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />}
</HorizontalGroup>
))}
{!filters.length && <AddFilter />}
</VerticalGroup>
</QueryEditorRow>
);
};

View File

@@ -1,14 +1,23 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { Project, VisualMetricQueryEditor, AliasBy } from '.';
import { MetricQuery, MetricDescriptor, EditorMode } from '../types';
import {
MetricQuery,
MetricDescriptor,
EditorMode,
MetricKind,
PreprocessorType,
AlignmentTypes,
CustomMetaData,
ValueTypes,
} from '../types';
import { getAlignmentPickerData } from '../functions';
import CloudMonitoringDatasource from '../datasource';
import { SelectableValue } from '@grafana/data';
import { MQLQueryEditor } from './MQLQueryEditor';
export interface Props {
refId: string;
usedAlignmentPeriod?: number;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onChange: (query: MetricQuery) => void;
onRunQuery: () => void;
@@ -29,16 +38,16 @@ export const defaultQuery: (dataSource: CloudMonitoringDatasource) => MetricQuer
editorMode: EditorMode.Visual,
projectName: dataSource.getDefaultProject(),
metricType: '',
metricKind: '',
metricKind: MetricKind.GAUGE,
valueType: '',
unit: '',
crossSeriesReducer: 'REDUCE_MEAN',
alignmentPeriod: 'cloud-monitoring-auto',
perSeriesAligner: 'ALIGN_MEAN',
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
groupBys: [],
filters: [],
aliasBy: '',
query: '',
preprocessor: PreprocessorType.None,
});
function Editor({
@@ -47,7 +56,7 @@ function Editor({
datasource,
onChange: onQueryChange,
onRunQuery,
usedAlignmentPeriod,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const [state, setState] = useState<State>(defaultState);
@@ -61,22 +70,32 @@ function Editor({
}
}, [datasource, groupBys, metricType, projectName, refId]);
const onChange = (metricQuery: MetricQuery) => {
onQueryChange({ ...query, ...metricQuery });
onRunQuery();
};
const onChange = useCallback(
(metricQuery: MetricQuery) => {
onQueryChange({ ...query, ...metricQuery });
onRunQuery();
},
[onQueryChange, onRunQuery, query]
);
const onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
{ valueType, metricKind, perSeriesAligner: state.perSeriesAligner },
datasource.templateSrv
);
setState({
...state,
alignOptions,
});
onChange({ ...query, perSeriesAligner, metricType: type, unit, valueType, metricKind });
};
const onMetricTypeChange = useCallback(
({ valueType, metricKind, type }: MetricDescriptor) => {
const preprocessor =
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
? PreprocessorType.None
: PreprocessorType.Rate;
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, state.perSeriesAligner, preprocessor);
onChange({
...query,
perSeriesAligner,
metricType: type,
valueType,
metricKind,
preprocessor,
});
},
[onChange, query, state]
);
return (
<>
@@ -93,7 +112,7 @@ function Editor({
<VisualMetricQueryEditor
labels={state.labels}
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
customMetaData={customMetaData}
onMetricTypeChange={onMetricTypeChange}
onChange={onChange}
datasource={datasource}

View File

@@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useState } from 'react';
import { startCase, uniqBy } from 'lodash';
import { Select } from '@grafana/ui';
import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow, QueryEditorField } from '.';
import CloudMonitoringDatasource from '../datasource';
import { Segment } from '@grafana/ui';
import { INNER_LABEL_WIDTH, LABEL_WIDTH, SELECT_WIDTH } from '../constants';
import { MetricDescriptor } from '../types';
export interface Props {
@@ -118,44 +120,39 @@ export function Metrics(props: Props) {
return (
<>
<div className="gf-form-inline">
<span className="gf-form-label width-9 query-keyword">Service</span>
<Segment
onChange={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">
<span className="gf-form-label width-9 query-keyword">Metric</span>
<QueryEditorRow>
<QueryEditorField labelWidth={LABEL_WIDTH} label="Service">
<Select
width={SELECT_WIDTH}
onChange={onServiceChange}
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
></Select>
</QueryEditorField>
<QueryEditorField label="Metric name" labelWidth={INNER_LABEL_WIDTH}>
<Select
width={SELECT_WIDTH}
onChange={onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
></Select>
</QueryEditorField>
</QueryEditorRow>
<Segment
className="query-part"
onChange={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>
</div>
{children(state.metricDescriptor)}
</>
);

View File

@@ -0,0 +1,65 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { RadioButtonGroup } from '@grafana/ui';
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../types';
import { getAlignmentPickerData } from '../functions';
import { QueryEditorRow } from '.';
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
export interface Props {
metricDescriptor?: MetricDescriptor;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
}
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
const options = useOptions(metricDescriptor);
return (
<QueryEditorRow
label="Pre-processing"
tooltip="Preprocessing options are displayed when the selected metric has a metric kind of delta or cumulative. The specific options available are determined by the metic's value type. If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series"
>
<RadioButtonGroup
onChange={(value: PreprocessorType) => {
const { valueType, metricKind, perSeriesAligner: psa } = query;
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value);
onChange({ ...query, preprocessor: value, perSeriesAligner });
}}
value={query.preprocessor ?? PreprocessorType.None}
options={options}
></RadioButtonGroup>
</QueryEditorRow>
);
};
const useOptions = (metricDescriptor?: MetricDescriptor): Array<SelectableValue<string>> => {
const metricKind = metricDescriptor?.metricKind;
const valueType = metricDescriptor?.valueType;
return useMemo(() => {
if (!metricKind || metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION) {
return [NONE_OPTION];
}
const options = [
NONE_OPTION,
{
label: 'Rate',
value: PreprocessorType.Rate,
description: 'Data points are aligned and converted to a rate per time series',
},
];
return metricKind === MetricKind.CUMULATIVE
? [
...options,
{
label: 'Delta',
value: PreprocessorType.Delta,
description: 'Data points are aligned by their delta (difference) per time series',
},
]
: options;
}, [metricKind, valueType]);
};

View File

@@ -1,7 +1,9 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { SegmentAsync } from '@grafana/ui';
import { Select } from '@grafana/ui';
import CloudMonitoringDatasource from '../datasource';
import { SELECT_WIDTH } from '../constants';
import { QueryEditorRow } from '.';
export interface Props {
datasource: CloudMonitoringDatasource;
@@ -11,27 +13,30 @@ export interface Props {
}
export function Project({ projectName, datasource, onChange, templateVariableOptions }: Props) {
const [projects, setProjects] = useState<Array<SelectableValue<string>>>([]);
useEffect(() => {
datasource.getProjects().then((projects) =>
setProjects([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...projects,
])
);
}, [datasource, templateVariableOptions]);
return (
<div className="gf-form-inline">
<span className="gf-form-label width-9 query-keyword">Project</span>
<SegmentAsync
<QueryEditorRow label="Project">
<Select
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use project: ${v}`}
onChange={({ value }) => onChange(value!)}
loadOptions={() =>
datasource.getProjects().then((projects) => [
{
label: 'Template Variables',
options: templateVariableOptions,
},
...projects,
])
}
value={projectName}
options={projects}
value={{ value: projectName, label: projectName }}
placeholder="Select Project"
/>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</QueryEditorRow>
);
}

View File

@@ -1,24 +1,18 @@
import React, { PureComponent } from 'react';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { ExploreQueryFieldProps, SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { Help, MetricQueryEditor, SLOQueryEditor } from './';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, queryTypes, EditorMode } from '../types';
import { css } from '@emotion/css';
import { ExploreQueryFieldProps } from '@grafana/data';
import { Button, Select } from '@grafana/ui';
import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types';
import { SELECT_WIDTH, QUERY_TYPES } from '../constants';
import { defaultQuery } from './MetricQueryEditor';
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
import { formatCloudMonitoringError, toOption } from '../functions';
import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor';
import { toOption } from '../functions';
import CloudMonitoringDatasource from '../datasource';
export type Props = ExploreQueryFieldProps<CloudMonitoringDatasource, CloudMonitoringQuery>;
interface State {
lastQueryError: string;
}
export class QueryEditor extends PureComponent<Props, State> {
state: State = { lastQueryError: '' };
export class QueryEditor extends PureComponent<Props> {
async UNSAFE_componentWillMount() {
const { datasource, query } = this.props;
@@ -39,24 +33,6 @@ export class QueryEditor extends PureComponent<Props, State> {
}
}
componentDidMount() {
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
appEvents.on(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
}
componentWillUnmount() {
appEvents.off(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
}
onDataResponse() {
this.setState({ lastQueryError: '' });
}
onDataError(error: any) {
this.setState({ lastQueryError: formatCloudMonitoringError(error) });
}
onQueryChange(prop: string, value: any) {
this.props.onChange({ ...this.props.query, [prop]: value });
this.props.onRunQuery();
@@ -68,7 +44,7 @@ export class QueryEditor extends PureComponent<Props, State> {
const sloQuery = { ...defaultSLOQuery(datasource), ...query.sloQuery };
const queryType = query.queryType || QueryType.METRICS;
const meta = this.props.data?.series.length ? this.props.data?.series[0].meta : {};
const usedAlignmentPeriod = meta?.alignmentPeriod;
const customMetaData = meta?.custom ?? {};
const variableOptionGroup = {
label: 'Template Variables',
expanded: false,
@@ -77,48 +53,44 @@ export class QueryEditor extends PureComponent<Props, State> {
return (
<>
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Query Type</label>
<Segment
value={[...queryTypes, ...variableOptionGroup.options].find((qt) => qt.value === queryType)}
options={[
...queryTypes,
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
]}
onChange={({ value }: SelectableValue<QueryType>) => {
<QueryEditorRow
label="Query type"
fillComponent={
query.queryType !== QueryType.SLO && (
<Button
variant="secondary"
className={css`
margin-left: auto;
`}
icon="edit"
onClick={() =>
this.onQueryChange('metricQuery', {
...metricQuery,
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
})
}
>
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
</Button>
)
}
>
<Select
width={SELECT_WIDTH}
value={queryType}
options={QUERY_TYPES}
onChange={({ value }) => {
onChange({ ...query, sloQuery, queryType: value! });
onRunQuery();
}}
/>
{query.queryType !== QueryType.SLO && (
<button
className="gf-form-label "
onClick={() =>
this.onQueryChange('metricQuery', {
...metricQuery,
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
})
}
>
<span className="query-keyword">{'<>'}</span>&nbsp;&nbsp;
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
</button>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
</QueryEditorRow>
{queryType === QueryType.METRICS && (
<MetricQueryEditor
refId={query.refId}
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
customMetaData={customMetaData}
onChange={(metricQuery: MetricQuery) => {
this.props.onChange({ ...this.props.query, metricQuery });
}}
@@ -131,18 +103,13 @@ export class QueryEditor extends PureComponent<Props, State> {
{queryType === QueryType.SLO && (
<SLOQueryEditor
variableOptionGroup={variableOptionGroup}
usedAlignmentPeriod={usedAlignmentPeriod}
customMetaData={customMetaData}
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
onRunQuery={onRunQuery}
datasource={datasource}
query={sloQuery}
></SLOQueryEditor>
)}
<Help
rawQuery={decodeURIComponent(meta?.executedQueryString ?? '')}
lastQueryError={this.state.lastQueryError}
/>
</>
);
}

View File

@@ -1,7 +1,8 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { QueryType, queryTypes } from '../types';
import { QueryType } from '../types';
import { QUERY_TYPES } from '../constants';
export interface Props {
value: QueryType;
@@ -14,9 +15,9 @@ export const QueryTypeSelector: FunctionComponent<Props> = ({ onChange, value, t
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Query Type</label>
<Segment
value={[...queryTypes, ...templateVariableOptions].find((qt) => qt.value === value)}
value={[...QUERY_TYPES, ...templateVariableOptions].find((qt) => qt.value === value)}
options={[
...queryTypes,
...QUERY_TYPES,
{
label: 'Template Variables',
options: templateVariableOptions,

View File

@@ -0,0 +1,52 @@
import React, { useEffect, useState } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow } from '..';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const SLO: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
const [slos, setSLOs] = useState<Array<SelectableValue<string>>>([]);
const { projectName, serviceId } = query;
useEffect(() => {
if (!projectName || !serviceId) {
return;
}
datasource.getServiceLevelObjectives(projectName, serviceId).then((sloIds: Array<SelectableValue<string>>) => {
setSLOs([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...sloIds,
]);
});
}, [datasource, projectName, serviceId, templateVariableOptions]);
return (
<QueryEditorRow label="SLO">
<Select
width={SELECT_WIDTH}
allowCustomValue
value={query?.sloId && { value: query?.sloId, label: query?.sloName || query?.sloId }}
placeholder="Select SLO"
options={slos}
onChange={async ({ value: sloId = '', label: sloName = '' }) => {
const slos = await datasource.getServiceLevelObjectives(projectName, serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, sloName, goal: slo?.goal });
}}
/>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { Project, AliasBy, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorRow } from '..';
import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types';
import CloudMonitoringDatasource from '../../datasource';
import { Selector, Service, SLO } from '.';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: CloudMonitoringDatasource;
}
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
projectName: dataSource.getDefaultProject(),
alignmentPeriod: 'cloud-monitoring-auto',
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
serviceName: '',
sloId: '',
sloName: '',
});
export function SLOQueryEditor({
query,
datasource,
onChange,
variableOptionGroup,
customMetaData,
}: React.PropsWithChildren<Props>) {
return (
<>
<Project
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={(projectName) => onChange({ ...query, projectName })}
/>
<Service
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></Service>
<SLO
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></SLO>
<Selector
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
onChange={onChange}
></Selector>
<QueryEditorRow label="Alignment period">
<AlignmentPeriod
templateVariableOptions={variableOptionGroup.options}
query={{
...query,
perSeriesAligner: query.selectorName === 'select_slo_health' ? 'ALIGN_MEAN' : 'ALIGN_NEXT_OLDER',
}}
onChange={onChange}
selectWidth={SELECT_WIDTH}
/>
<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />
</QueryEditorRow>
<AliasBy value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
</>
);
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow } from '..';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECT_WIDTH, SELECTORS } from '../../constants';
export interface Props {
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const Selector: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
return (
<QueryEditorRow label="Selector">
<Select
width={SELECT_WIDTH}
allowCustomValue
value={[...SELECTORS, ...templateVariableOptions].find((s) => s.value === query?.selectorName ?? '')}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...SELECTORS,
]}
onChange={({ value: selectorName }) => onChange({ ...query, selectorName: selectorName ?? '' })}
/>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,50 @@
import React, { useEffect, useState } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { QueryEditorRow } from '..';
import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types';
import { SELECT_WIDTH } from '../../constants';
export interface Props {
onChange: (query: SLOQuery) => void;
query: SLOQuery;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
}
export const Service: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
const { projectName } = query;
useEffect(() => {
if (!projectName) {
return;
}
datasource.getSLOServices(projectName).then((services: Array<SelectableValue<string>>) => {
setServices([
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]);
});
}, [datasource, projectName, templateVariableOptions]);
return (
<QueryEditorRow label="Service">
<Select
width={SELECT_WIDTH}
allowCustomValue
value={query?.serviceId && { value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
placeholder="Select service"
options={services}
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
onChange({ ...query, serviceId, serviceName, sloId: '' })
}
/>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,3 @@
export { Service } from './Service';
export { SLO } from './SLO';
export { Selector } from './Selector';

View File

@@ -1,112 +0,0 @@
import React from 'react';
import { Segment, SegmentAsync } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { selectors } from '../constants';
import { Project, AlignmentPeriods, AliasBy, QueryInlineField } from '.';
import { SLOQuery } from '../types';
import CloudMonitoringDatasource from '../datasource';
export interface Props {
usedAlignmentPeriod?: number;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: CloudMonitoringDatasource;
}
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
projectName: dataSource.getDefaultProject(),
alignmentPeriod: 'cloud-monitoring-auto',
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
serviceName: '',
sloId: '',
sloName: '',
});
export function SLOQueryEditor({
query,
datasource,
onChange,
variableOptionGroup,
usedAlignmentPeriod,
}: React.PropsWithChildren<Props>) {
return (
<>
<Project
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={(projectName) => onChange({ ...query, projectName })}
/>
<QueryInlineField label="Service">
<SegmentAsync
allowCustomValue
value={{ value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
placeholder="Select service"
loadOptions={() =>
datasource.getSLOServices(query.projectName).then((services) => [
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...services,
])
}
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
onChange({ ...query, serviceId, serviceName, sloId: '' })
}
/>
</QueryInlineField>
<QueryInlineField label="SLO">
<SegmentAsync
allowCustomValue
value={{ value: query?.sloId, label: query?.sloName || query?.sloId }}
placeholder="Select SLO"
loadOptions={() =>
datasource.getServiceLevelObjectives(query.projectName, query.serviceId).then((sloIds) => [
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...sloIds,
])
}
onChange={async ({ value: sloId = '', label: sloName = '' }) => {
const slos = await datasource.getServiceLevelObjectives(query.projectName, query.serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, sloName, goal: slo?.goal });
}}
/>
</QueryInlineField>
<QueryInlineField label="Selector">
<Segment
allowCustomValue
value={[...selectors, ...variableOptionGroup.options].find((s) => s.value === query?.selectorName ?? '')}
options={[
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...selectors,
]}
onChange={({ value: selectorName }) => onChange({ ...query, selectorName })}
/>
</QueryInlineField>
<AlignmentPeriods
templateSrv={datasource.templateSrv}
templateVariableOptions={variableOptionGroup.options}
alignmentPeriod={query.alignmentPeriod || ''}
perSeriesAligner={query.selectorName === 'select_slo_health' ? 'ALIGN_MEAN' : 'ALIGN_NEXT_OLDER'}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={(alignmentPeriod) => onChange({ ...query, alignmentPeriod })}
/>
<AliasBy value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
</>
);
}

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { Aggregations, Metrics, LabelFilter, GroupBys, Alignments, AlignmentPeriods } from '.';
import { MetricQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import CloudMonitoringDatasource from '../datasource';
import { SelectableValue } from '@grafana/data';
import { Metrics, LabelFilter, GroupBy, Preprocessor, Alignment } from '.';
import { MetricQuery, MetricDescriptor, CustomMetaData } from '../types';
import CloudMonitoringDatasource from '../datasource';
export interface Props {
usedAlignmentPeriod?: number;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onMetricTypeChange: (query: MetricDescriptor) => void;
onChange: (query: MetricQuery) => void;
@@ -21,11 +20,9 @@ function Editor({
datasource,
onChange,
onMetricTypeChange,
usedAlignmentPeriod,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(query, datasource.templateSrv);
return (
<Metrics
templateSrv={datasource.templateSrv}
@@ -40,40 +37,23 @@ function Editor({
<LabelFilter
labels={labels}
filters={query.filters!}
onChange={(filters) => onChange({ ...query, filters })}
onChange={(filters: string[]) => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={query.groupBys!}
onChange={(groupBys) => onChange({ ...query, groupBys })}
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
<GroupBy
labels={Object.keys(labels)}
query={query}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
/>
<Alignment
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
crossSeriesReducer={query.crossSeriesReducer}
groupBys={query.groupBys!}
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
>
{(displayAdvancedOptions) =>
displayAdvancedOptions && (
<Alignments
alignOptions={alignOptions}
templateVariableOptions={variableOptionGroup.options}
perSeriesAligner={perSeriesAligner || ''}
onChange={(perSeriesAligner) => onChange({ ...query, perSeriesAligner })}
/>
)
}
</Aggregations>
<AlignmentPeriods
templateSrv={datasource.templateSrv}
templateVariableOptions={variableOptionGroup.options}
alignmentPeriod={query.alignmentPeriod || ''}
perSeriesAligner={query.perSeriesAligner || ''}
usedAlignmentPeriod={usedAlignmentPeriod}
onChange={(alignmentPeriod) => onChange({ ...query, alignmentPeriod })}
query={query}
customMetaData={customMetaData}
onChange={onChange}
/>
</>
)}

View File

@@ -1,16 +1,18 @@
export { Project } from './Project';
export { Metrics } from './Metrics';
export { Help } from './Help';
export { GroupBys } from './GroupBys';
export { GroupBy } from './GroupBy';
export { Alignment } from './Alignment';
export { LabelFilter } from './LabelFilter';
export { AnnotationsHelp } from './AnnotationsHelp';
export { Alignments } from './Alignments';
export { AlignmentPeriods } from './AlignmentPeriods';
export { AlignmentFunction } from './AlignmentFunction';
export { AlignmentPeriod } from './AlignmentPeriod';
export { AlignmentPeriodLabel } from './AlignmentPeriodLabel';
export { AliasBy } from './AliasBy';
export { Aggregations } from './Aggregations';
export { Aggregation } from './Aggregation';
export { MetricQueryEditor } from './MetricQueryEditor';
export { SLOQueryEditor } from './SLOQueryEditor';
export { SLOQueryEditor } from './SLO/SLOQueryEditor';
export { MQLQueryEditor } from './MQLQueryEditor';
export { QueryTypeSelector } from './QueryType';
export { QueryInlineField, QueryField, VariableQueryField } from './Fields';
export { VariableQueryField, QueryEditorRow, QueryEditorField } from './Fields';
export { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
export { Preprocessor } from './Preprocessor';