Commit a111cc0d by Erik Sundell Committed by GitHub

Stackdriver: Support for SLO queries (#22917)

* wip: add slo support

* Export DataSourcePlugin

* wip: break out metric query editor into its own component

* wip: refactor frontend - keep SLO and Metric query in differnt objects

* wip - load services and slos

* Fix broken test

* Add interactive slo expression builder

* Change order of dropdowns

* Refactoring backend model. slo unit testing in progress

* Unit test migration and SLOs

* Cleanup SLO editor

* Simplify alias by component

* Support alias by for slos

* Support slos in variable queries

* Fix broken last query error

* Update Help section to include SLO aliases

* streamline datasource resource cache

* Break out api specific stuff in datasource to its own file

* Move get projects call to frontend

* Refactor api caching

* Unit test api service

* Fix lint go issue

* Fix typescript strict errors

* Fix test datasource

* Use budget fraction selector instead of budget

* Reset SLO when service is changed

* Handle error in case resource call returned no data

* Show real SLI display name

* Use unsafe prefix on will mount hook

* Store goal in query model since it will be used as soon as graph panel supports adding a threshold

* Add comment to describe why componentWillMount is used

* Interpolate sloid

* Break out SLO aggregation into its own func

* Also test group bys for metricquery test

* Remove not used type fields

* Remove annoying stackdriver prefix from error message

* Default view param to FULL

* Add part about SLO query builder in docs

* Use new images

* Fixes after feedback

* Add one more group by test

* Make stackdriver types internal

* Update docs/sources/features/datasources/stackdriver.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update docs/sources/features/datasources/stackdriver.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Update docs/sources/features/datasources/stackdriver.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Updates after PR feedback

* add test for when no alias by defined

* fix infinite loop when newVariables feature flag is on

onChange being called in componentDidUpdate produces an
infinite loop when using the new React template variable
implementation.

Also fixes a spelling mistake

* implements feedback for documentation changes

* more doc changes

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Daniel Lee <dan.limerick@gmail.com>
parent e19493ae
......@@ -34,7 +34,7 @@ func (e *StackdriverExecutor) executeAnnotationQuery(ctx context.Context, tsdbQu
return result, err
}
func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery, title string, text string, tags string) error {
func (e *StackdriverExecutor) parseToAnnotations(queryRes *tsdb.QueryResult, data stackdriverResponse, query *stackdriverQuery, title string, text string, tags string) error {
annotations := make([]map[string]string, 0)
for _, series := range data.TimeSeries {
......
......@@ -18,7 +18,7 @@ func TestStackdriverAnnotationQuery(t *testing.T) {
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "annotationQuery"}
query := &StackdriverQuery{}
query := &stackdriverQuery{}
err = executor.parseToAnnotations(res, data, query, "atitle {{metric.label.instance_name}} {{metric.value}}", "atext {{resource.label.zone}}", "atag")
So(err, ShouldBeNil)
......
{
"timeSeries": [{
"metric": {
"type": "select_slo_compliance(\"projects/test-proj/services/test-service/serviceLevelObjectives/test-slo\")"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "114250375703598695",
"project_id": "test-proj"
}
},
"metricKind": "DELTA",
"valueType": "INT64"
}
]
}
......@@ -6,17 +6,49 @@ import (
)
type (
// StackdriverQuery is the query that Grafana sends from the frontend
StackdriverQuery struct {
stackdriverQuery struct {
Target string
Params url.Values
RefID string
GroupBys []string
AliasBy string
ProjectName string
Selector string
Service string
Slo string
}
StackdriverBucketOptions struct {
metricQuery struct {
ProjectName string
MetricType string
CrossSeriesReducer string
AlignmentPeriod string
PerSeriesAligner string
GroupBys []string
Filters []string
AliasBy string
View string
}
sloQuery struct {
ProjectName string
AlignmentPeriod string
PerSeriesAligner string
AliasBy string
SelectorName string
ServiceId string
SloId string
}
grafanaQuery struct {
DatasourceId int
RefId string
QueryType string
MetricQuery metricQuery
SloQuery sloQuery
}
stackdriverBucketOptions struct {
LinearBuckets *struct {
NumFiniteBuckets int64 `json:"numFiniteBuckets"`
Width int64 `json:"width"`
......@@ -32,8 +64,7 @@ type (
} `json:"explicitBuckets"`
}
// StackdriverResponse is the data returned from the external Google Stackdriver API
StackdriverResponse struct {
stackdriverResponse struct {
TimeSeries []struct {
Metric struct {
Labels map[string]string `json:"labels"`
......@@ -64,7 +95,7 @@ type (
Min int `json:"min"`
Max int `json:"max"`
} `json:"range"`
BucketOptions StackdriverBucketOptions `json:"bucketOptions"`
BucketOptions stackdriverBucketOptions `json:"bucketOptions"`
BucketCounts []string `json:"bucketCounts"`
Examplars []struct {
Value float64 `json:"value"`
......@@ -76,18 +107,4 @@ type (
} `json:"points"`
} `json:"timeSeries"`
}
// ResourceManagerProjectList is the data returned from the external Google Resource Manager API
ResourceManagerProjectList struct {
Projects []ResourceManagerProject `json:"projects"`
}
ResourceManagerProject struct {
ProjectID string `json:"projectId"`
}
ResourceManagerProjectSelect struct {
Label string `json:"label"`
Value string `json:"value"`
}
)
import isString from 'lodash/isString';
import { alignmentPeriods, ValueTypes, MetricKind } from './constants';
import { alignmentPeriods, ValueTypes, MetricKind, selectors } from './constants';
import StackdriverDatasource from './datasource';
import { MetricFindQueryTypes, VariableQueryData } from './types';
import { SelectableValue } from '@grafana/data';
import {
getMetricTypesByService,
getAlignmentOptionsByMetric,
......@@ -38,6 +39,12 @@ export default class StackdriverMetricFindQuery {
return this.handleAlignmentPeriodQuery();
case MetricFindQueryTypes.Aggregations:
return this.handleAggregationQuery(query);
case MetricFindQueryTypes.SLOServices:
return this.handleSLOServicesQuery(query);
case MetricFindQueryTypes.SLO:
return this.handleSLOQuery(query);
case MetricFindQueryTypes.Selectors:
return this.handleSelectorQuery();
default:
return [];
}
......@@ -49,7 +56,7 @@ export default class StackdriverMetricFindQuery {
async handleProjectsQuery() {
const projects = await this.datasource.getProjects();
return projects.map((s: { label: string; value: string }) => ({
return (projects as SelectableValue<string>).map((s: { label: string; value: string }) => ({
text: s.label,
value: s.value,
expandable: true,
......@@ -130,6 +137,20 @@ export default class StackdriverMetricFindQuery {
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map(this.toFindQueryResult);
}
async handleSLOServicesQuery({ projectName }: VariableQueryData) {
const services = await this.datasource.getSLOServices(projectName);
return services.map(this.toFindQueryResult);
}
async handleSLOQuery({ selectedSLOService, projectName }: VariableQueryData) {
const slos = await this.datasource.getServiceLevelObjectives(projectName, selectedSLOService);
return slos.map(this.toFindQueryResult);
}
async handleSelectorQuery() {
return selectors.map(this.toFindQueryResult);
}
handleAlignmentPeriodQuery() {
return alignmentPeriods.map(this.toFindQueryResult);
}
......
import Api from './api';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { SelectableValue } from '@grafana/data';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
const response = [
{ label: 'test1', value: 'test1' },
{ label: 'test2', value: 'test2' },
];
describe('api', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
beforeEach(() => {
datasourceRequestMock.mockImplementation((options: any) => {
const data = { [options.url.match(/([^\/]*)\/*$/)[1]]: response };
return Promise.resolve({ data, status: 200 });
});
});
describe('when resource was cached', () => {
let api: Api;
let res: Array<SelectableValue<string>>;
beforeEach(async () => {
api = new Api('/stackdriver/');
api.cache['some-resource'] = response;
res = await api.get('some-resource');
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response);
expect(api.cache['some-resource']).toEqual(response);
expect(datasourceRequestMock).not.toHaveBeenCalled();
});
});
describe('when resource was not cached', () => {
let api: Api;
let res: Array<SelectableValue<string>>;
beforeEach(async () => {
api = new Api('/stackdriver/');
res = await api.get('some-resource');
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response);
expect(api.cache['some-resource']).toEqual(response);
expect(datasourceRequestMock).toHaveBeenCalled();
});
});
describe('when cache should be bypassed', () => {
let api: Api;
let res: Array<SelectableValue<string>>;
beforeEach(async () => {
api = new Api('/stackdriver/');
api.cache['some-resource'] = response;
res = await api.get('some-resource', { useCache: false });
});
it('should return cached value and not load from source', () => {
expect(res).toEqual(response);
expect(datasourceRequestMock).toHaveBeenCalled();
});
});
});
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { formatStackdriverError } from './functions';
import { MetricDescriptor } from './types';
interface Options {
responseMap?: (res: any) => SelectableValue<string> | MetricDescriptor;
baseUrl?: string;
useCache?: boolean;
}
export default class Api {
cache: { [key: string]: Array<SelectableValue<string>> };
defaultOptions: Options;
constructor(private baseUrl: string) {
this.cache = {};
this.defaultOptions = {
useCache: true,
responseMap: (res: any) => res,
baseUrl: this.baseUrl,
};
}
async get(path: string, options?: Options): Promise<Array<SelectableValue<string>> | MetricDescriptor[]> {
try {
const { useCache, responseMap, baseUrl } = { ...this.defaultOptions, ...options };
if (useCache && this.cache[path]) {
return this.cache[path];
}
const response = await getBackendSrv().datasourceRequest({
url: baseUrl + path,
method: 'GET',
});
const responsePropName = path.match(/([^\/]*)\/*$/)[1];
let res = [];
if (response && response.data && response.data[responsePropName]) {
res = response.data[responsePropName].map(responseMap);
}
if (useCache) {
this.cache[path] = res;
}
return res;
} catch (error) {
appEvents.emit(CoreEvents.dsRequestError, { error: { data: { error: formatStackdriverError(error) } } });
return [];
}
}
async post(data: { [key: string]: any }) {
return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data,
});
}
async test(projectName: string) {
return getBackendSrv().datasourceRequest({
url: `${this.baseUrl}${projectName}/metricDescriptors`,
method: 'GET',
});
}
}
......@@ -5,10 +5,9 @@ import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { ValueTypes, MetricKind } from '../constants';
import { MetricDescriptor } from '../types';
export interface Props {
onChange: (metricDescriptor: MetricDescriptor[]) => void;
onChange: (metricDescriptor: string) => void;
metricDescriptor: {
valueType: string;
metricKind: string;
......@@ -92,7 +91,7 @@ export class Aggregations extends React.Component<Props, State> {
</label>
</div>
</div>
{this.props.children(this.state.displayAdvancedOptions)}
{this.props.children && this.props.children(this.state.displayAdvancedOptions)}
</>
);
}
......
import React, { Component } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { debounce } from 'lodash';
import { Input } from '@grafana/ui';
import { QueryInlineField } from '.';
export interface Props {
onChange: (alignmentPeriod: string) => void;
onChange: (alias: any) => void;
value: string;
}
export interface State {
value: string;
}
export class AliasBy extends Component<Props, State> {
propagateOnChange: (value: any) => void;
export const AliasBy: FunctionComponent<Props> = ({ value = '', onChange }) => {
const [alias, setAlias] = useState(value);
constructor(props: Props) {
super(props);
this.propagateOnChange = debounce(this.props.onChange, 500);
this.state = { value: '' };
}
const propagateOnChange = debounce(onChange, 1000);
componentDidMount() {
this.setState({ value: this.props.value });
}
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (nextProps.value !== this.props.value) {
this.setState({ value: nextProps.value });
}
}
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ value: e.target.value });
this.propagateOnChange(e.target.value);
onChange = (e: any) => {
setAlias(e.target.value);
propagateOnChange(e.target.value);
};
render() {
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<label className="gf-form-label query-keyword width-9">Alias By</label>
<Input type="text" className="gf-form-input width-24" value={this.state.value} onChange={this.onChange} />
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</>
<QueryInlineField label="Alias By">
<input type="text" className="gf-form-input width-26" value={alias} onChange={onChange} />
</QueryInlineField>
);
}
}
};
......@@ -36,7 +36,7 @@ export const AlignmentPeriods: FC<Props> = ({
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<Segment
onChange={({ value }) => onChange(value)}
onChange={({ value }) => onChange(value!)}
value={[...options, ...templateVariableOptions].find(s => s.value === alignmentPeriod)}
options={[
{
......
......@@ -17,7 +17,7 @@ export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOption
<div className="gf-form offset-width-9">
<label className="gf-form-label query-keyword width-15">Aligner</label>
<Segment
onChange={({ value }) => onChange(value)}
onChange={({ value }) => onChange(value!)}
value={[...alignOptions, ...templateVariableOptions].find(s => s.value === perSeriesAligner)}
options={[
{
......
......@@ -5,7 +5,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from '../datasource';
import { Metrics, Filters, AnnotationsHelp, Project } from './';
import { Metrics, LabelFilter, AnnotationsHelp, Project } from './';
import { toOption } from '../functions';
import { AnnotationTarget, MetricDescriptor } from '../types';
......@@ -41,7 +41,9 @@ const DefaultTarget: State = {
export class AnnotationQueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
async componentDidMount() {
async UNSAFE_componentWillMount() {
// Unfortunately, migrations like this need to go componentWillMount. As soon as there's
// migration hook for this module.ts, we can do the migrations there instead.
const { target, datasource } = this.props;
if (!target.projectName) {
target.projectName = datasource.getDefaultProject();
......@@ -86,7 +88,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
}
render() {
const { projectName, metricType, filters, title, text, variableOptionGroup, labels, variableOptions } = this.state;
const { metricType, projectName, filters, title, text, variableOptionGroup, labels, variableOptions } = this.state;
const { datasource } = this.props;
return (
......@@ -107,7 +109,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
>
{metric => (
<>
<Filters
<LabelFilter
labels={labels}
filters={filters}
onChange={value => this.onChange('filters', value)}
......
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormLabel } from '@grafana/ui';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
tooltip?: string;
children?: React.ReactNode;
}
export const QueryField: FunctionComponent<Partial<Props>> = ({ label, tooltip, children }) => (
<>
<FormLabel width={9} className="query-keyword" tooltip={tooltip}>
{label}
</FormLabel>
{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>
);
};
......@@ -26,7 +26,7 @@ export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [],
key={value + index}
value={value}
options={options}
onChange={({ value }) =>
onChange={({ value = '' }) =>
onChange(
value === removeText
? values.filter((_, i) => i !== index)
......@@ -43,7 +43,7 @@ export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [],
</a>
}
allowCustomValue
onChange={({ value }) => onChange([...values, value])}
onChange={({ value = '' }) => onChange([...values, value])}
options={[
variableOptionGroup,
...labelsToGroupedOptions([...groupBys.filter(groupBy => !values.includes(groupBy)), ...systemLabels]),
......
......@@ -105,6 +105,18 @@ export class Help extends React.Component<Props, State> {
<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>
......
......@@ -2,7 +2,7 @@ import React, { FunctionComponent, Fragment } from 'react';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { labelsToGroupedOptions, toOption } from '../functions';
import { labelsToGroupedOptions, filtersToStringArray, stringArrayToFilters, toOption } from '../functions';
import { Filter } from '../types';
export interface Props {
......@@ -15,18 +15,8 @@ export interface Props {
const removeText = '-- remove filter --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'fa fa-remove' };
const operators = ['=', '!=', '=~', '!=~'];
const filtersToStringArray = (filters: Filter[]) =>
_.flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition]));
const stringArrayToFilters = (filterArray: string[]) =>
_.chunk(filterArray, 4).map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}));
export const Filters: FunctionComponent<Props> = ({
export const LabelFilter: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
......@@ -45,7 +35,7 @@ export const Filters: FunctionComponent<Props> = ({
allowCustomValue
value={key}
options={options}
onChange={({ value: key }) => {
onChange={({ value: key = '' }) => {
if (key === removeText) {
onChange(filtersToStringArray(filters.filter((_, i) => i !== index)));
} else {
......@@ -61,7 +51,7 @@ export const Filters: FunctionComponent<Props> = ({
value={operator}
className="gf-form-label query-segment-operator"
options={operators.map(toOption)}
onChange={({ value: operator }) =>
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
/>
......@@ -72,7 +62,7 @@ export const Filters: FunctionComponent<Props> = ({
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value }) =>
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
......@@ -90,7 +80,7 @@ export const Filters: FunctionComponent<Props> = ({
</a>
}
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key }) =>
onChange={({ value: key = '' }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' } as Filter]))
}
/>
......
import React, { useState, useEffect } from 'react';
import { Project, Aggregations, Metrics, LabelFilter, GroupBys, Alignments, AlignmentPeriods, AliasBy } from '.';
import { MetricQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import StackdriverDatasource from '../datasource';
import { SelectableValue } from '@grafana/data';
export interface Props {
refId: string;
usedAlignmentPeriod: string;
variableOptionGroup: SelectableValue<string>;
onChange: (query: MetricQuery) => void;
onRunQuery: () => void;
query: MetricQuery;
datasource: StackdriverDatasource;
}
interface State {
labels: any;
[key: string]: any;
}
export const defaultState: State = {
labels: {},
};
export const defaultQuery: MetricQuery = {
projectName: '',
metricType: '',
metricKind: '',
valueType: '',
unit: '',
crossSeriesReducer: 'REDUCE_MEAN',
alignmentPeriod: 'stackdriver-auto',
perSeriesAligner: 'ALIGN_MEAN',
groupBys: [],
filters: [],
aliasBy: '',
};
function Editor({
refId,
query,
datasource,
onChange,
usedAlignmentPeriod,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
const [state, setState] = useState<State>(defaultState);
useEffect(() => {
if (query && query.projectName && query.metricType) {
datasource
.getLabels(query.metricType, refId, query.projectName, query.groupBys)
.then(labels => setState({ ...state, labels }));
}
}, [query.projectName, query.groupBys, query.metricType]);
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 { labels } = state;
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(query, datasource.templateSrv);
return (
<>
<Project
templateVariableOptions={variableOptionGroup.options}
projectName={query.projectName}
datasource={datasource}
onChange={projectName => {
onChange({ ...query, projectName });
}}
/>
<Metrics
templateSrv={datasource.templateSrv}
projectName={query.projectName}
metricType={query.metricType}
templateVariableOptions={variableOptionGroup.options}
datasource={datasource}
onChange={onMetricTypeChange}
>
{metric => (
<>
<LabelFilter
labels={labels}
filters={query.filters!}
onChange={filters => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={query.groupBys!}
onChange={groupBys => onChange({ ...query, groupBys })}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
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 })}
/>
<AliasBy value={query.aliasBy || ''} onChange={aliasBy => onChange({ ...query, aliasBy })} />
</>
)}
</Metrics>
</>
);
}
export const MetricQueryEditor = React.memo(Editor);
......@@ -81,8 +81,8 @@ export function Metrics(props: Props) {
const { metricType, templateSrv } = props;
const metrics = metricDescriptors
.filter(m => m.service === templateSrv.replace(service))
.map(m => ({
.filter((m: MetricDescriptor) => m.service === templateSrv.replace(service))
.map((m: MetricDescriptor) => ({
service: m.service,
value: m.type,
label: m.displayName,
......@@ -96,7 +96,7 @@ export function Metrics(props: Props) {
}
};
const onMetricTypeChange = ({ value }: any, extra: any = {}) => {
const onMetricTypeChange = ({ value }: SelectableValue<string>, extra: any = {}) => {
const metricDescriptor = getSelectedMetricDescriptor(state.metricDescriptors, value);
setState({ ...state, metricDescriptor, ...extra });
props.onChange({ ...metricDescriptor, type: value });
......
import React from 'react';
import renderer from 'react-test-renderer';
import { DefaultTarget, Props, QueryEditor } from './QueryEditor';
import { TemplateSrv } from 'app/features/templating/template_srv';
const props: Props = {
onQueryChange: target => {},
onExecuteQuery: () => {},
target: DefaultTarget,
events: { on: () => {} },
datasource: {
getProjects: () => Promise.resolve([]),
getDefaultProject: () => Promise.resolve('projectName'),
ensureGCEDefaultProject: () => {},
getMetricTypes: () => Promise.resolve([]),
getLabels: () => Promise.resolve([]),
variables: [],
} as any,
templateSrv: new TemplateSrv(),
};
describe('QueryEditor', () => {
it('renders correctly', () => {
const tree = renderer.create(<QueryEditor {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});
});
import React, { FunctionComponent } from 'react';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { QueryType, queryTypes } from '../types';
export interface Props {
value: QueryType;
onChange: (slo: QueryType) => void;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const QueryTypeSelector: FunctionComponent<Props> = ({ onChange, value, templateVariableOptions }) => {
return (
<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)}
options={[
...queryTypes,
{
label: 'Template Variables',
options: templateVariableOptions,
},
]}
onChange={({ value }: SelectableValue<QueryType>) => onChange(value!)}
/>
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};
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 StackdriverDatasource from '../datasource';
export interface Props {
usedAlignmentPeriod: string;
variableOptionGroup: SelectableValue<string>;
onChange: (query: SLOQuery) => void;
onRunQuery: () => void;
query: SLOQuery;
datasource: StackdriverDatasource;
}
export const defaultQuery: SLOQuery = {
projectName: '',
alignmentPeriod: 'stackdriver-auto',
aliasBy: '',
selectorName: 'select_slo_health',
serviceId: '',
sloId: '',
};
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={query?.serviceId}
placeholder="Select service"
loadOptions={() =>
datasource.getSLOServices(query.projectName).then(services => [
{
label: 'Template Variables',
options: variableOptionGroup.options,
},
...services,
])
}
onChange={({ value: serviceId = '' }) => onChange({ ...query, serviceId, sloId: '' })}
/>
</QueryInlineField>
<QueryInlineField label="SLO">
<SegmentAsync
allowCustomValue
value={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 = '' }) => {
const slos = await datasource.getServiceLevelObjectives(query.projectName, query.serviceId);
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
onChange({ ...query, sloId, 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 })} />
</>
);
}
......@@ -16,8 +16,10 @@ const props: VariableQueryProps = {
query: {},
datasource: {
getDefaultProject: () => '',
getProjects: async (): Promise<any[]> => [],
getMetricTypes: async (p: any): Promise<any[]> => [],
getProjects: async () => Promise.resolve([]),
getMetricTypes: async (projectName: string) => Promise.resolve([]),
getSLOServices: async (projectName: string, serviceId: string) => Promise.resolve([]),
getServiceLevelObjectives: (projectName: string, serviceId: string) => Promise.resolve([]),
},
templateSrv: { replace: (s: string) => s, getVariables: () => ([] as unknown) as VariableModel[] },
};
......
......@@ -3,6 +3,7 @@ import { VariableQueryProps } from 'app/types/plugins';
import { SimpleSelect } from './';
import { extractServicesFromMetricDescriptors, getLabelKeys, getMetricTypes } from '../functions';
import { MetricFindQueryTypes, VariableQueryData } from '../types';
import { getConfig } from 'app/core/config';
export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
queryTypes: Array<{ value: string; name: string }> = [
......@@ -15,6 +16,9 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
{ value: MetricFindQueryTypes.Aggregations, name: 'Aggregations' },
{ value: MetricFindQueryTypes.Aligners, name: 'Aligners' },
{ value: MetricFindQueryTypes.AlignmentPeriods, name: 'Alignment Periods' },
{ value: MetricFindQueryTypes.Selectors, name: 'Selectors' },
{ value: MetricFindQueryTypes.SLOServices, name: 'SLO Services' },
{ value: MetricFindQueryTypes.SLO, name: 'Service Level Objectives (SLO)' },
];
defaults: VariableQueryData = {
......@@ -26,6 +30,8 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
labelKey: '',
metricTypes: [],
services: [],
sloServices: [],
selectedSLOService: '',
projects: [],
projectName: '',
};
......@@ -63,6 +69,8 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
this.props.templateSrv.replace(selectedService)
);
const sloServices = await this.props.datasource.getSLOServices(this.state.projectName);
const state: any = {
services,
selectedService,
......@@ -71,6 +79,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
metricDescriptors,
projects: projects.map(({ value, label }: any) => ({ value, name: label })),
...(await this.getLabels(selectedMetricType, this.state.projectName)),
sloServices: sloServices.map(({ value, label }: any) => ({ value, name: label })),
};
this.setState(state);
}
......@@ -80,6 +89,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
selectedQueryType: queryType,
...(await this.getLabels(this.state.selectedMetricType, this.state.projectName, queryType)),
};
this.setState(state);
}
......@@ -93,7 +103,16 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
this.props.templateSrv.replace(this.state.selectedService)
);
this.setState({ ...labels, metricTypes, selectedMetricType, metricDescriptors, projectName });
const sloServices = await this.props.datasource.getSLOServices(projectName);
this.setState({
...labels,
metricTypes,
selectedMetricType,
metricDescriptors,
projectName,
sloServices: sloServices.map(({ value, label }: any) => ({ value, name: label })),
});
}
async onServiceChange(service: string) {
......@@ -124,11 +143,13 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
this.setState({ labelKey });
}
componentDidUpdate() {
componentDidUpdate(prevProps: Readonly<VariableQueryProps>, prevState: Readonly<VariableQueryData>) {
if (!getConfig().featureToggles.newVariables || prevState.selectedQueryType !== this.state.selectedQueryType) {
const { metricDescriptors, labels, metricTypes, services, ...queryModel } = this.state;
const query = this.queryTypes.find(q => q.value === this.state.selectedQueryType);
this.props.onChange(queryModel, `Stackdriver - ${query.name}`);
}
}
async getLabels(selectedMetricType: string, projectName: string, selectedQueryType = this.state.selectedQueryType) {
let result = { labels: this.state.labels, labelKey: this.state.labelKey };
......@@ -220,6 +241,40 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
/>
</>
);
case MetricFindQueryTypes.SLOServices:
return (
<>
<SimpleSelect
value={this.state.projectName}
options={this.insertTemplateVariables(this.state.projects)}
onValueChange={e => this.onProjectChange(e.target.value)}
label="Project"
/>
</>
);
case MetricFindQueryTypes.SLO:
return (
<>
<SimpleSelect
value={this.state.projectName}
options={this.insertTemplateVariables(this.state.projects)}
onValueChange={e => this.onProjectChange(e.target.value)}
label="Project"
/>
<SimpleSelect
value={this.state.selectedSLOService}
options={this.insertTemplateVariables(this.state.sloServices)}
onValueChange={e => {
this.setState({
...this.state,
selectedSLOService: e.target.value,
});
}}
label="SLO Service"
/>
</>
);
default:
return '';
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QueryEditor renders correctly 1`] = `
Array [
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Project
</span>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Select Project
</a>
</div>
<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"
>
Service
</span>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Select Services
</a>
</div>
<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>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder query-part"
>
Select Metric
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Filter
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Group By
</label>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Aggregation
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Select Reducer
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
>
<a
onClick={[Function]}
>
<i
className="fa fa-caret-right"
/>
Advanced Options
</a>
</label>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Alignment Period
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
stackdriver auto
</a>
</div>
<div
className="gf-form gf-form--grow"
>
</div>
</div>,
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<label
className="gf-form-label query-keyword width-9"
>
Alias By
</label>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input width-24"
onChange={[Function]}
type="text"
value=""
/>
</div>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<div
className="gf-form"
onClick={[Function]}
>
<label
className="gf-form-label query-keyword pointer"
>
Show Help
<i
className="fa fa-caret-right"
/>
</label>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
"",
"",
]
`;
......@@ -64,6 +64,21 @@ Array [
>
Alignment Periods
</option>
<option
value="selectors"
>
Selectors
</option>
<option
value="sloServices"
>
SLO Services
</option>
<option
value="slo"
>
Service Level Objectives (SLO)
</option>
</select>
</div>
</div>,
......
......@@ -2,10 +2,14 @@ export { Project } from './Project';
export { Metrics } from './Metrics';
export { Help } from './Help';
export { GroupBys } from './GroupBys';
export { Filters } from './Filters';
export { LabelFilter } from './LabelFilter';
export { AnnotationsHelp } from './AnnotationsHelp';
export { Alignments } from './Alignments';
export { AlignmentPeriods } from './AlignmentPeriods';
export { AliasBy } from './AliasBy';
export { Aggregations } from './Aggregations';
export { SimpleSelect } from './SimpleSelect';
export { MetricQueryEditor } from './MetricQueryEditor';
export { SLOQueryEditor } from './SLOQueryEditor';
export { QueryTypeSelector } from './QueryType';
export { QueryInlineField, QueryField } from './Fields';
......@@ -273,3 +273,9 @@ export const systemLabels = [
'metadata.system_labels.top_level_controller_name',
'metadata.system_labels.container_image',
];
export const selectors = [
{ label: 'SLI Value', value: 'select_slo_health' },
{ label: 'SLO Compliance', value: 'select_slo_compliance' },
{ label: 'SLO Error Budget Remaining', value: 'select_slo_budget_fraction' },
];
......@@ -3,7 +3,7 @@ import { alignOptions, aggOptions, ValueTypes, MetricKind, systemLabels } from '
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from './datasource';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { StackdriverQuery, MetricDescriptor } from './types';
import { MetricDescriptor, Filter, MetricQuery } from './types';
export const extractServicesFromMetricDescriptors = (metricDescriptors: MetricDescriptor[]) =>
_.uniqBy(metricDescriptors, 'service');
......@@ -61,7 +61,7 @@ export const getLabelKeys = async (
};
export const getAlignmentPickerData = (
{ valueType, metricKind, perSeriesAligner }: Partial<StackdriverQuery>,
{ valueType, metricKind, perSeriesAligner }: Partial<MetricQuery>,
templateSrv: TemplateSrv
) => {
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!).map(option => ({
......@@ -92,4 +92,36 @@ export const labelsToGroupedOptions = (groupBys: string[]) => {
return Object.entries(groups).map(([label, options]) => ({ label, options, expanded: true }), []);
};
export const filtersToStringArray = (filters: Filter[]) => {
const strArr = _.flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition]));
return strArr.filter((_, i) => i !== strArr.length - 1);
};
export const stringArrayToFilters = (filterArray: string[]) =>
_.chunk(filterArray, 4).map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}));
export const toOption = (value: string) => ({ label: value, value } as SelectableValue<string>);
export const formatStackdriverError = (error: any) => {
let message = error.statusText ?? '';
if (error.data && error.data.error) {
try {
const res = JSON.parse(error.data.error);
message += res.error.code + '. ' + res.error.message;
} catch (err) {
message += error.data.error;
}
} else if (error.data && error.data.message) {
try {
message = JSON.parse(error.data.message).error.message;
} catch (err) {
error.error;
}
}
return message;
};
import { DataSourcePlugin } from '@grafana/data';
import StackdriverDatasource from './datasource';
import { StackdriverQueryCtrl } from './query_ctrl';
import { QueryEditor } from './components/QueryEditor';
import { StackdriverConfigCtrl } from './config_ctrl';
import { StackdriverAnnotationsQueryCtrl } from './annotations_query_ctrl';
import { StackdriverVariableQueryEditor } from './components/VariableQueryEditor';
import { StackdriverQuery } from './types';
export {
StackdriverDatasource as Datasource,
StackdriverQueryCtrl as QueryCtrl,
StackdriverConfigCtrl as ConfigCtrl,
StackdriverAnnotationsQueryCtrl as AnnotationsQueryCtrl,
StackdriverVariableQueryEditor as VariableQueryEditor,
};
export const plugin = new DataSourcePlugin<StackdriverDatasource, StackdriverQuery>(StackdriverDatasource)
.setQueryEditor(QueryEditor)
.setConfigCtrl(StackdriverConfigCtrl)
.setAnnotationQueryCtrl(StackdriverAnnotationsQueryCtrl)
.setVariableQueryEditor(StackdriverVariableQueryEditor);
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
<stackdriver-query-editor
target="ctrl.target"
events="ctrl.panelCtrl.events"
datasource="ctrl.datasource"
template-srv="ctrl.templateSrv"
on-query-change="(ctrl.onQueryChange)"
on-execute-query="(ctrl.onExecuteQuery)"
></stackdriver-query-editor>
</query-editor-row>
import { QueryCtrl } from 'app/plugins/sdk';
import { StackdriverQuery } from './types';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { auto } from 'angular';
export class StackdriverQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
templateSrv: TemplateSrv;
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, templateSrv: TemplateSrv) {
super($scope, $injector);
this.templateSrv = templateSrv;
this.onQueryChange = this.onQueryChange.bind(this);
this.onExecuteQuery = this.onExecuteQuery.bind(this);
}
onQueryChange(target: StackdriverQuery) {
Object.assign(this.target, target);
}
onExecuteQuery() {
this.$scope.ctrl.refresh();
}
}
......@@ -3,7 +3,7 @@ import { metricDescriptors } from './testData';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all';
import { DataSourceInstanceSettings, toUtc } from '@grafana/data';
import { StackdriverOptions, StackdriverQuery } from '../types';
import { StackdriverOptions } from '../types';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
......@@ -180,7 +180,7 @@ describe('StackdriverDataSource', () => {
});
it('should replace the variable with the value', () => {
expect(interpolated.length).toBe(4);
expect(interpolated.length).toBe(3);
expect(interpolated[2]).toBe('filtervalue1');
});
});
......@@ -193,7 +193,7 @@ describe('StackdriverDataSource', () => {
});
it('should replace the variable with the value and not with regex formatting', () => {
expect(interpolated.length).toBe(4);
expect(interpolated.length).toBe(3);
expect(interpolated[0]).toBe('resource.label.zone');
});
});
......@@ -250,7 +250,7 @@ describe('StackdriverDataSource', () => {
describe('when theres only one target', () => {
describe('and the stackdriver unit doesnt have a corresponding grafana unit', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
});
it('should return undefined', () => {
expect(res).toBeUndefined();
......@@ -258,7 +258,7 @@ describe('StackdriverDataSource', () => {
});
describe('and the stackdriver unit has a corresponding grafana unit', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }]);
});
it('should return bits', () => {
expect(res).toEqual('bits');
......@@ -269,7 +269,7 @@ describe('StackdriverDataSource', () => {
describe('when theres more than one target', () => {
describe('and all target units are the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'bit' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'bit' }]);
});
it('should return bits', () => {
expect(res).toEqual('bits');
......@@ -277,10 +277,7 @@ describe('StackdriverDataSource', () => {
});
describe('and all target units are the same but doesnt have grafana mappings', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([
{ unit: 'megaseconds' },
{ unit: 'megaseconds' },
] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
});
it('should return the default value of undefined', () => {
expect(res).toBeUndefined();
......@@ -288,7 +285,7 @@ describe('StackdriverDataSource', () => {
});
describe('and all target units are not the same', () => {
beforeEach(() => {
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }] as StackdriverQuery[]);
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
});
it('should return the default value of undefined', () => {
expect(res).toBeUndefined();
......
......@@ -21,6 +21,9 @@ export enum MetricFindQueryTypes {
Aggregations = 'aggregations',
Aligners = 'aligners',
AlignmentPeriods = 'alignmentPeriods',
Selectors = 'selectors',
SLOServices = 'sloServices',
SLO = 'slo',
}
export interface VariableQueryData {
......@@ -28,32 +31,60 @@ export interface VariableQueryData {
metricDescriptors: MetricDescriptor[];
selectedService: string;
selectedMetricType: string;
selectedSLOService: string;
labels: string[];
labelKey: string;
metricTypes: Array<{ value: string; name: string }>;
services: Array<{ value: string; name: string }>;
projects: Array<{ value: string; name: string }>;
sloServices: Array<{ value: string; name: string }>;
projectName: string;
}
export interface StackdriverQuery extends DataQuery {
export enum QueryType {
METRICS = 'metrics',
SLO = 'slo',
}
export const queryTypes = [
{ label: 'Metrics', value: QueryType.METRICS },
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
];
export interface MetricQuery {
projectName: string;
unit?: string;
metricType: string;
service?: string;
refId: string;
crossSeriesReducer: string;
alignmentPeriod?: string;
perSeriesAligner: string;
perSeriesAligner?: string;
groupBys?: string[];
filters?: string[];
aliasBy?: string;
metricKind: string;
valueType: string;
datasourceId?: number;
metricKind?: string;
valueType?: string;
view?: string;
}
export interface SLOQuery {
projectName: string;
alignmentPeriod?: string;
perSeriesAligner?: string;
aliasBy?: string;
selectorName: string;
serviceId: string;
sloId: string;
goal?: number;
}
export interface StackdriverQuery extends DataQuery {
datasourceId?: number;
refId: string;
queryType: QueryType;
metricQuery: MetricQuery;
sloQuery?: SLOQuery;
}
export interface StackdriverOptions extends DataSourceJsonData {
defaultProject?: string;
gceDefaultProject?: string;
......@@ -100,5 +131,5 @@ export interface Filter {
key: string;
operator: string;
value: string;
condition: string;
condition?: string;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment