mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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);
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
</>
|
||||
|
||||
@@ -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: <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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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: <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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Service } from './Service';
|
||||
export { SLO } from './SLO';
|
||||
export { Selector } from './Selector';
|
||||
@@ -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 })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user