mirror of https://github.com/grafana/grafana
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>pull/23155/head
parent
e19493ae24
commit
a111cc0d5c
@ -0,0 +1,17 @@ |
||||
{ |
||||
"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" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,69 @@ |
||||
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(); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,72 @@ |
||||
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', |
||||
}); |
||||
} |
||||
} |
@ -1,53 +1,25 @@ |
||||
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> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
return ( |
||||
<QueryInlineField label="Alias By"> |
||||
<input type="text" className="gf-form-input width-26" value={alias} onChange={onChange} /> |
||||
</QueryInlineField> |
||||
); |
||||
}; |
||||
|
@ -0,0 +1,28 @@ |
||||
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> |
||||
); |
||||
}; |
@ -0,0 +1,140 @@ |
||||
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); |
@ -1,27 +0,0 @@ |
||||
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(); |
||||
}); |
||||
}); |
@ -0,0 +1,34 @@ |
||||
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> |
||||
); |
||||
}; |
@ -0,0 +1,108 @@ |
||||
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 })} /> |
||||
</> |
||||
); |
||||
} |
@ -1,248 +0,0 @@ |
||||
// 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>, |
||||
"", |
||||
"", |
||||
] |
||||
`; |
@ -1,13 +1,13 @@ |
||||
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); |
||||
|
@ -1,10 +0,0 @@ |
||||
<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> |
@ -1,25 +0,0 @@ |
||||
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(); |
||||
} |
||||
} |
Loading…
Reference in new issue