Merge branch 'master' into azure-monitor-refactor-#15087

This commit is contained in:
corpglory-dev
2019-02-06 14:31:23 +03:00
208 changed files with 29479 additions and 4137 deletions

View File

@@ -232,6 +232,14 @@ export default class CloudWatchDatasource {
});
}
getResourceARNs(region, resourceType, tags) {
return this.doMetricQueryRequest('resource_arns', {
region: this.templateSrv.replace(this.getActualRegion(region)),
resourceType: this.templateSrv.replace(resourceType),
tags: tags,
});
}
metricFindQuery(query) {
let region;
let namespace;
@@ -293,6 +301,15 @@ export default class CloudWatchDatasource {
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
}
const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
if (resourceARNsQuery) {
region = resourceARNsQuery[1];
const resourceType = resourceARNsQuery[2];
const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
return this.getResourceARNs(region, resourceType, tagsJSON);
}
return this.$q.when([]);
}

View File

@@ -380,6 +380,29 @@ describe('CloudWatchDatasource', () => {
});
});
describeMetricFindQuery('resource_arns(default,ec2:instance,{"environment":["production"]})', scenario => {
scenario.setup(() => {
scenario.requestResponse = {
results: {
metricFindQuery: {
tables: [{
rows: [[
'arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567',
'arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321'
]]
}],
},
},
};
});
it('should call __ListMetrics and return result', () => {
expect(scenario.result[0].text).toContain('arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567');
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
expect(scenario.request.queries[0].subtype).toBe('resource_arns');
});
});
it('should caclculate the correct period', () => {
const hourSec = 60 * 60;
const daySec = hourSec * 24;

View File

@@ -0,0 +1,86 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { Select, SelectOptionItem } from '@grafana/ui';
// Types
import { QueryEditorProps } from '@grafana/ui/src/types';
import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types';
import { LokiQueryField } from './LokiQueryField';
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
interface State {
query: LokiQuery;
}
export class LokiQueryEditor extends PureComponent<Props> {
state: State = {
query: this.props.query,
};
onRunQuery = () => {
const { query } = this.state;
this.props.onChange(query);
this.props.onRunQuery();
};
onFieldChange = (query: LokiQuery, override?) => {
this.setState({
query: {
...this.state.query,
expr: query.expr,
},
});
};
onFormatChanged = (option: SelectOptionItem) => {
this.props.onChange({
...this.state.query,
resultFormat: option.value,
});
};
render() {
const { query } = this.state;
const { datasource } = this.props;
const formatOptions: SelectOptionItem[] = [
{ label: 'Time Series', value: 'time_series' },
{ label: 'Table', value: 'table' },
];
query.resultFormat = query.resultFormat || 'time_series';
const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
return (
<div>
<LokiQueryField
datasource={datasource}
query={query}
onQueryChange={this.onFieldChange}
onExecuteQuery={this.onRunQuery}
history={[]}
/>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label">Format as</div>
<Select
isSearchable={false}
options={formatOptions}
onChange={this.onFormatChanged}
value={currentFormat}
/>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</div>
);
}
}
export default LokiQueryEditor;

View File

@@ -15,8 +15,9 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
// Types
import { LokiQuery } from '../types';
import { TypeaheadOutput } from 'app/types/explore';
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise';
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
const PRISM_SYNTAX = 'promql';
@@ -64,15 +65,8 @@ interface CascaderOption {
disabled?: boolean;
}
interface LokiQueryFieldProps {
datasource: any;
error?: string | JSX.Element;
hint?: any;
history?: any[];
initialQuery?: LokiQuery;
onClickHintFix?: (action: any) => void;
onPressEnter?: () => void;
onQueryChange?: (value: LokiQuery, override?: boolean) => void;
interface LokiQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> {
history: HistoryItem[];
}
interface LokiQueryFieldState {
@@ -80,7 +74,7 @@ interface LokiQueryFieldState {
syntaxLoaded: boolean;
}
class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
plugins: any[];
pluginsSearch: any[];
languageProvider: any;
@@ -97,14 +91,14 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
this.plugins = [
BracesPlugin(),
RunnerPlugin({ handler: props.onPressEnter }),
RunnerPlugin({ handler: props.onExecuteQuery }),
PluginPrism({
onlyIn: node => node.type === 'code_block',
getSyntax: node => 'promql',
}),
];
this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })];
this.state = {
logLabelOptions: [],
@@ -168,20 +162,21 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
onChangeQuery = (value: string, override?: boolean) => {
// Send text change to parent
const { initialQuery, onQueryChange } = this.props;
const { query, onQueryChange, onExecuteQuery } = this.props;
if (onQueryChange) {
const query = {
...initialQuery,
expr: value,
};
onQueryChange(query, override);
const nextQuery = { ...query, expr: value };
onQueryChange(nextQuery);
if (override && onExecuteQuery) {
onExecuteQuery();
}
}
};
onClickHintFix = () => {
const { hint, onClickHintFix } = this.props;
if (onClickHintFix && hint && hint.fix) {
onClickHintFix(hint.fix.action);
const { hint, onExecuteHint } = this.props;
if (onExecuteHint && hint && hint.fix) {
onExecuteHint(hint.fix.action);
}
};
@@ -219,7 +214,7 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
};
render() {
const { error, hint, initialQuery } = this.props;
const { error, hint, query } = this.props;
const { logLabelOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
@@ -239,10 +234,11 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
<QueryField
additionalPlugins={this.plugins}
cleanText={cleanText}
initialQuery={initialQuery.expr}
initialQuery={query.expr}
onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.props.onExecuteQuery}
placeholder="Enter a Loki query"
portalOrigin="loki"
syntaxLoaded={syntaxLoaded}

View File

@@ -1,11 +1,8 @@
import React, { PureComponent } from 'react';
import LokiCheatSheet from './LokiCheatSheet';
import { ExploreStartPageProps } from '@grafana/ui';
interface Props {
onClickExample: () => void;
}
export default class LokiStartPage extends PureComponent<Props> {
export default class LokiStartPage extends PureComponent<ExploreStartPageProps> {
render() {
return (
<div className="grafana-info-box grafana-info-box--max-lg">

View File

@@ -7,6 +7,17 @@ describe('LokiDatasource', () => {
url: 'myloggingurl',
};
const testResp = {
data: {
streams: [
{
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
labels: '{}',
},
],
},
};
describe('when querying', () => {
const backendSrvMock = { datasourceRequest: jest.fn() };
@@ -17,7 +28,7 @@ describe('LokiDatasource', () => {
test('should use default max lines when no limit given', () => {
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn();
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
ds.query(options);
@@ -30,7 +41,7 @@ describe('LokiDatasource', () => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn();
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
ds.query(options);
@@ -38,6 +49,34 @@ describe('LokiDatasource', () => {
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
});
test('should return log streams when resultFormat is undefined', async done => {
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: 'foo', refId: 'B' }],
});
const res = await ds.query(options);
expect(res.data[0].entries[0].line).toBe('hello');
done();
});
test('should return time series when resultFormat is time_series', async done => {
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: 'foo', refId: 'B', resultFormat: 'time_series' }],
});
const res = await ds.query(options);
expect(res.data[0].datapoints).toBeDefined();
done();
});
});
describe('when performing testDataSource', () => {

View File

@@ -32,7 +32,7 @@ function serializeParams(data: any) {
.join('&');
}
export default class LokiDatasource {
export class LokiDatasource {
languageProvider: LanguageProvider;
maxLines: number;
@@ -73,10 +73,11 @@ export default class LokiDatasource {
};
}
query(options: DataQueryOptions<LokiQuery>): Promise<{ data: LogsStream[] }> {
async query(options: DataQueryOptions<LokiQuery>) {
const queryTargets = options.targets
.filter(target => target.expr)
.filter(target => target.expr && !target.hide)
.map(target => this.prepareQueryTarget(target, options));
if (queryTargets.length === 0) {
return Promise.resolve({ data: [] });
}
@@ -84,20 +85,29 @@ export default class LokiDatasource {
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
return Promise.all(queries).then((results: any[]) => {
// Flatten streams from multiple queries
const allStreams: LogsStream[] = results.reduce((acc, response, i) => {
if (!response) {
return acc;
const allStreams: LogsStream[] = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
const query = queryTargets[i];
// add search term to stream & add to array
if (result.data) {
for (const stream of (result.data.streams || [])) {
stream.search = query.regexp;
allStreams.push(stream);
}
}
const streams: LogsStream[] = response.data.streams || [];
// Inject search for match highlighting
const search: string = queryTargets[i].regexp;
streams.forEach(s => {
s.search = search;
});
return [...acc, ...streams];
}, []);
return { data: allStreams };
}
// check resultType
if (options.targets[0].resultFormat === 'time_series') {
const logs = mergeStreamsToLogs(allStreams, this.maxLines);
logs.series = makeSeriesForLogs(logs.rows, options.intervalMs);
return { data: logs.series };
} else {
return { data: allStreams };
}
});
}
@@ -142,34 +152,36 @@ export default class LokiDatasource {
testDatasource() {
return this._request('/api/prom/label')
.then(res => {
if (res && res.data && res.data.values && res.data.values.length > 0) {
return { status: 'success', message: 'Data source connected and labels found.' };
}
return {
status: 'error',
message:
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
};
})
.catch(err => {
let message = 'Loki: ';
if (err.statusText) {
message += err.statusText;
} else {
message += 'Cannot connect to Loki';
}
.then(res => {
if (res && res.data && res.data.values && res.data.values.length > 0) {
return { status: 'success', message: 'Data source connected and labels found.' };
}
return {
status: 'error',
message:
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
};
})
.catch(err => {
let message = 'Loki: ';
if (err.statusText) {
message += err.statusText;
} else {
message += 'Cannot connect to Loki';
}
if (err.status) {
message += `. ${err.status}`;
}
if (err.status) {
message += `. ${err.status}`;
}
if (err.data && err.data.message) {
message += `. ${err.data.message}`;
} else if (err.data) {
message += `. ${err.data}`;
}
return { status: 'error', message: message };
});
if (err.data && err.data.message) {
message += `. ${err.data.message}`;
} else if (err.data) {
message += `. ${err.data}`;
}
return { status: 'error', message: message };
});
}
}
export default LokiDatasource;

View File

@@ -2,6 +2,7 @@ import Datasource from './datasource';
import LokiStartPage from './components/LokiStartPage';
import LokiQueryField from './components/LokiQueryField';
import LokiQueryEditor from './components/LokiQueryEditor';
export class LokiConfigCtrl {
static templateUrl = 'partials/config.html';
@@ -9,6 +10,7 @@ export class LokiConfigCtrl {
export {
Datasource,
LokiQueryEditor as QueryEditor,
LokiConfigCtrl as ConfigCtrl,
LokiQueryField as ExploreQueryField,
LokiStartPage as ExploreStartPage,

View File

@@ -2,12 +2,14 @@
"type": "datasource",
"name": "Loki",
"id": "loki",
"metrics": false,
"metrics": true,
"alerting": false,
"annotations": false,
"logs": true,
"explore": true,
"tables": false,
"info": {
"description": "Loki Logging Data Source for Grafana",
"author": {

View File

@@ -2,5 +2,8 @@ import { DataQuery } from '@grafana/ui/src/types';
export interface LokiQuery extends DataQuery {
expr: string;
resultFormat?: LokiQueryResultFormats;
}
export type LokiQueryResultFormats = 'time_series' | 'logs';

View File

@@ -4,7 +4,7 @@ import Cascader from 'rc-cascader';
import PluginPrism from 'slate-prism';
import Prism from 'prismjs';
import { TypeaheadOutput } from 'app/types/explore';
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
// dom also includes Element polyfills
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
@@ -13,6 +13,7 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
import { PromQuery } from '../types';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
const HISTOGRAM_GROUP = '__histograms__';
const METRIC_MARK = 'metric';
@@ -86,15 +87,8 @@ interface CascaderOption {
disabled?: boolean;
}
interface PromQueryFieldProps {
datasource: any;
error?: string | JSX.Element;
initialQuery: PromQuery;
hint?: any;
history?: any[];
onClickHintFix?: (action: any) => void;
onPressEnter?: () => void;
onQueryChange?: (value: PromQuery, override?: boolean) => void;
interface PromQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, PromQuery> {
history: HistoryItem[];
}
interface PromQueryFieldState {
@@ -116,7 +110,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
this.plugins = [
BracesPlugin(),
RunnerPlugin({ handler: props.onPressEnter }),
RunnerPlugin({ handler: props.onExecuteQuery }),
PluginPrism({
onlyIn: node => node.type === 'code_block',
getSyntax: node => 'promql',
@@ -174,20 +168,21 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
onChangeQuery = (value: string, override?: boolean) => {
// Send text change to parent
const { initialQuery, onQueryChange } = this.props;
const { query, onQueryChange, onExecuteQuery } = this.props;
if (onQueryChange) {
const query: PromQuery = {
...initialQuery,
expr: value,
};
onQueryChange(query, override);
const nextQuery: PromQuery = { ...query, expr: value };
onQueryChange(nextQuery);
if (override && onExecuteQuery) {
onExecuteQuery();
}
}
};
onClickHintFix = () => {
const { hint, onClickHintFix } = this.props;
if (onClickHintFix && hint && hint.fix) {
onClickHintFix(hint.fix.action);
const { hint, onExecuteHint } = this.props;
if (onExecuteHint && hint && hint.fix) {
onExecuteHint(hint.fix.action);
}
};
@@ -242,29 +237,30 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
};
render() {
const { error, hint, initialQuery } = this.props;
const { error, hint, query } = this.props;
const { metricsOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = syntaxLoaded ? 'Metrics' : 'Loading metrics...';
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-inline gf-form-inline--nowrap">
<div className="gf-form flex-shrink-0">
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
{chooserText} <i className="fa fa-caret-down" />
</button>
</Cascader>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form gf-form--grow flex-shrink-1">
<QueryField
additionalPlugins={this.plugins}
cleanText={cleanText}
initialQuery={initialQuery.expr}
initialQuery={query.expr}
onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.props.onExecuteQuery}
placeholder="Enter a PromQL query"
portalOrigin="prometheus"
syntaxLoaded={syntaxLoaded}

View File

@@ -1,11 +1,8 @@
import React, { PureComponent } from 'react';
import PromCheatSheet from './PromCheatSheet';
import { ExploreStartPageProps } from '@grafana/ui';
interface Props {
onClickExample: () => void;
}
export default class PromStart extends PureComponent<Props> {
export default class PromStart extends PureComponent<ExploreStartPageProps> {
render() {
return (
<div className="grafana-info-box grafana-info-box--max-lg">

View File

@@ -10,21 +10,21 @@ import { Alignments } from './Alignments';
import { AlignmentPeriods } from './AlignmentPeriods';
import { AliasBy } from './AliasBy';
import { Help } from './Help';
import { Target, MetricDescriptor } from '../types';
import { StackdriverQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import StackdriverDatasource from '../datasource';
import { SelectOptionItem } from '@grafana/ui';
export interface Props {
onQueryChange: (target: Target) => void;
onQueryChange: (target: StackdriverQuery) => void;
onExecuteQuery: () => void;
target: Target;
target: StackdriverQuery;
events: any;
datasource: StackdriverDatasource;
templateSrv: TemplateSrv;
}
interface State extends Target {
interface State extends StackdriverQuery {
alignOptions: SelectOptionItem[];
lastQuery: string;
lastQueryError: string;

View File

@@ -2,9 +2,10 @@ import { stackdriverUnitMappings } from './constants';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
import { MetricDescriptor } from './types';
import { StackdriverQuery, MetricDescriptor } from './types';
import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types';
export default class StackdriverDatasource {
export default class StackdriverDatasource implements DataSourceApi<StackdriverQuery> {
id: number;
url: string;
baseUrl: string;
@@ -39,9 +40,7 @@ export default class StackdriverDatasource {
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
view: t.view || 'FULL',
filters: (t.filters || []).map(f => {
return this.templateSrv.replace(f, options.scopedVars || {});
}),
filters: this.interpolateFilters(t.filters, options.scopedVars),
aliasBy: this.templateSrv.replace(t.aliasBy, options.scopedVars || {}),
type: 'timeSeriesQuery',
};
@@ -63,7 +62,13 @@ export default class StackdriverDatasource {
}
}
async getLabels(metricType, refId) {
interpolateFilters(filters: string[], scopedVars: object) {
return (filters || []).map(f => {
return this.templateSrv.replace(f, scopedVars || {}, 'regex');
});
}
async getLabels(metricType: string, refId: string) {
const response = await this.getTimeSeries({
targets: [
{
@@ -103,7 +108,7 @@ export default class StackdriverDatasource {
return unit;
}
async query(options) {
async query(options: DataQueryOptions<StackdriverQuery>) {
const result = [];
const data = await this.getTimeSeries(options);
if (data.results) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
<svg width="100" height="90" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M-1-1h102v92H-1z"/><g><path d="M151.637 29.785c-1.659.621-3.32 1.241-4.783 2.055-1.548-7.686-18.278-8.18-18.117 1.021.148 8.228 18.35 8.414 22.9 16.065 3.456 5.808 1.064 14.28-3.417 17.433-1.805 1.271-4.625 3.234-10.936 3.076-7.568-.19-13.65-5.277-16.065-12.305 1.474-1.151 3.464-1.777 5.468-2.393.087 9.334 18.304 12.687 20.509 3.418 3.661-15.375-24.686-9.097-24.267-25.636.375-14.998 25.388-16.197 28.708-2.734zM207.347 68.413h-5.466v-4.444c-2.872 2.517-5.263 5.222-10.254 5.467-10.316.51-17.038-10.377-10.256-17.773 4.38-4.774 13.169-5.41 20.512-2.05 1.548-10.171-13.626-11.842-16.407-4.44-1.698-.697-3.195-1.592-5.126-2.054 2.832-10.246 20.01-9.729 24.949-2.392 4.608 6.837.757 17.618 2.048 27.686zm-22.216-7.866c4.483 6.856 17.435 2.377 16.751-6.154-5.161-3.469-18.501-3.389-16.751 6.154zM416.873 53.029c-7.868.794-17.201.117-25.638.343-1.48 10.76 16.123 14.618 19.144 5.127 1.631.754 3.326 1.457 5.127 2.048-2.477 9.824-18.37 11.251-25.294 4.445-9.549-9.386-4.276-31.335 12.987-29.735 8.89.826 13.149 7.176 13.674 17.772zm-25.295-4.444h18.801c-.04-11.168-18.433-9.957-18.801 0zM347.486 36.283v32.13h-5.81v-32.13h5.81zM352.273 36.283h6.153c3.048 8.342 6.48 16.303 9.224 24.949 4.33-7.408 6.575-16.895 10.251-24.949h6.155c-4.39 10.646-8.865 21.217-12.988 32.13h-6.152c-3.907-11.019-8.635-21.217-12.643-32.13zM427.354 36.225h-5.525v32.111h5.982V48.885s1.845-9.322 11.396-7.021l2.417-5.867s-8.978-2.532-14.155 5.407l-.115-5.179zM322.434 36.225h-5.522v32.111h5.987V48.885s1.84-9.322 11.395-7.021l2.417-5.867s-8.976-2.532-14.159 5.407l-.118-5.179zM304.139 51.998c0 6.579-4.645 11.919-10.372 11.919-5.725 0-10.366-5.34-10.366-11.919 0-6.586 4.642-11.92 10.366-11.92 5.727 0 10.372 5.334 10.372 11.92zm-.107 11.916v4.19h5.742V21.038h-5.812v19.325c-2.789-3.472-6.805-5.649-11.269-5.649-8.424 0-15.253 7.768-15.253 17.341 0 9.576 6.829 17.344 15.253 17.344 4.496 0 8.536-2.21 11.33-5.724l.009.239z" fill="#6F6F6F"/><circle r="4.185" cy="25.306" cx="344.584" fill="#6F6F6F"/><path fill="#6F6F6F" d="M253.751 50.332l13.835-14.078h7.603l-12.337 12.711 13.21 19.321h-7.354l-10.346-14.959-4.738 4.861v10.225h-5.856V21.422h5.856v28.443zM236.855 46.471c-1.762-3.644-5.163-6.109-9.065-6.109-5.713 0-10.348 5.282-10.348 11.799 0 6.524 4.635 11.806 10.348 11.806 3.93 0 7.347-2.496 9.101-6.183l5.394 2.556c-2.779 5.419-8.227 9.097-14.497 9.097-9.083 0-16.451-7.733-16.451-17.275 0-9.537 7.368-17.269 16.451-17.269 6.247 0 11.683 3.653 14.467 9.041l-5.4 2.537zM160.884 26.693v9.747h-5.849v5.479h5.727v17.052s-.37 13.157 15.103 9.383l-1.947-4.995s-7.065 2.434-7.065-3.896V41.919h7.796V36.56h-7.674v-9.866h-6.091v-.001z"/><path fill="#009245" d="M50.794 41.715L27.708 84.812l46.207-.008z"/><path fill="#006837" d="M27.699 84.804L4.833 44.994h45.958z"/><path fill="#39B54A" d="M50.913 45.008H4.833L27.898 5.12H74.031z"/><path fill="#8CC63F" d="M74.031 5.12l23.236 39.84-23.352 39.844-23.002-39.796z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -14,8 +14,8 @@
"description": "Google Stackdriver Datasource for Grafana",
"version": "1.0.0",
"logos": {
"small": "img/stackdriver_logo.png",
"large": "img/stackdriver_logo.png"
"small": "img/stackdriver_logo.svg",
"large": "img/stackdriver_logo.svg"
},
"author": {
"name": "Grafana Project",

View File

@@ -1,7 +1,7 @@
import _ from 'lodash';
import { QueryCtrl } from 'app/plugins/sdk';
import { Target } from './types';
import { StackdriverQuery } from './types';
import { TemplateSrv } from 'app/features/templating/template_srv';
export class StackdriverQueryCtrl extends QueryCtrl {
@@ -16,7 +16,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
this.onExecuteQuery = this.onExecuteQuery.bind(this);
}
onQueryChange(target: Target) {
onQueryChange(target: StackdriverQuery) {
Object.assign(this.target, target);
}

View File

@@ -1,7 +1,8 @@
import StackdriverDataSource from '../datasource';
import { metricDescriptors } from './testData';
import moment from 'moment';
import { TemplateSrvStub } from 'test/specs/helpers';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all';
describe('StackdriverDataSource', () => {
const instanceSettings = {
@@ -9,7 +10,7 @@ describe('StackdriverDataSource', () => {
defaultProject: 'testproject',
},
};
const templateSrv = new TemplateSrvStub();
const templateSrv = new TemplateSrv();
const timeSrv = {};
describe('when performing testDataSource', () => {
@@ -154,15 +155,41 @@ describe('StackdriverDataSource', () => {
});
});
describe('when interpolating a template variable for the filter', () => {
let interpolated;
describe('and is single value variable', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv('filtervalue1');
const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {});
});
it('should replace the variable with the value', () => {
expect(interpolated.length).toBe(3);
expect(interpolated[2]).toBe('filtervalue1');
});
});
describe('and is multi value variable', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true);
const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {});
});
it('should replace the variable with a regex expression', () => {
expect(interpolated[2]).toBe('(filtervalue1|filtervalue2)');
});
});
});
describe('when interpolating a template variable for group bys', () => {
let interpolated;
describe('and is single value variable', () => {
beforeEach(() => {
templateSrv.data = {
test: 'groupby1',
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
const groupByTemplateSrv = initTemplateSrv('groupby1');
const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
@@ -174,10 +201,8 @@ describe('StackdriverDataSource', () => {
describe('and is multi value variable', () => {
beforeEach(() => {
templateSrv.data = {
test: 'groupby1,groupby2',
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
@@ -241,3 +266,19 @@ describe('StackdriverDataSource', () => {
});
});
});
function initTemplateSrv(values: any, multi = false) {
const templateSrv = new TemplateSrv();
templateSrv.init([
new CustomVariable(
{
name: 'test',
current: {
value: values,
},
multi: multi,
},
{}
),
]);
return templateSrv;
}

View File

@@ -1,3 +1,5 @@
import { DataQuery } from '@grafana/ui/src/types';
export enum MetricFindQueryTypes {
Services = 'services',
MetricTypes = 'metricTypes',
@@ -20,20 +22,22 @@ export interface VariableQueryData {
services: Array<{ value: string; name: string }>;
}
export interface Target {
defaultProject: string;
unit: string;
export interface StackdriverQuery extends DataQuery {
defaultProject?: string;
unit?: string;
metricType: string;
service: string;
service?: string;
refId: string;
crossSeriesReducer: string;
alignmentPeriod: string;
alignmentPeriod?: string;
perSeriesAligner: string;
groupBys: string[];
filters: string[];
aliasBy: string;
groupBys?: string[];
filters?: string[];
aliasBy?: string;
metricKind: string;
valueType: string;
datasourceId?: number;
view?: string;
}
export interface AnnotationTarget {

View File

@@ -41,9 +41,9 @@ export class QueryEditor extends PureComponent<Props> {
}
onScenarioChange = (item: SelectOptionItem) => {
this.props.onQueryChange({
this.props.onChange({
...this.props.query,
scenarioId: item.value,
...this.props.query
});
}