Merge branch 'master' into datasource-dashboards-to-react

This commit is contained in:
Peter Holmberg
2018-10-18 08:34:31 +02:00
27 changed files with 380 additions and 141 deletions

View File

@@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl {
defaults: any = {
type: 'email',
sendReminder: false,
disableResolveMessage: false,
frequency: '15m',
settings: {
httpMethod: 'POST',

View File

@@ -21,21 +21,28 @@
<gf-form-switch
class="gf-form"
label="Send on all alerts"
label-class="width-12"
label-class="width-14"
checked="ctrl.model.isDefault"
tooltip="Use this notification for all alerts">
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="Include image"
label-class="width-12"
label-class="width-14"
checked="ctrl.model.settings.uploadImage"
tooltip="Captures an image and include it in the notification">
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="Disable Resolve Message"
label-class="width-14"
checked="ctrl.model.disableResolveMessage"
tooltip="Disable the resolve message [OK] that is sent when alerting state returns to false">
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="Send reminders"
label-class="width-12"
label-class="width-14"
checked="ctrl.model.sendReminder"
tooltip="Send additional notifications for triggered alerts">
</gf-form-switch>

View File

@@ -96,11 +96,14 @@ describe('PromQueryField typeahead handling', () => {
it('returns label suggestions on label context but leaves out labels that already exist', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
<PromQueryField
{...defaultProps}
labelKeys={{ '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] }}
/>
).instance() as PromQueryField;
const value = Plain.deserialize('{job="foo",}');
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
const range = value.selection.merge({
anchorOffset: 11,
anchorOffset: 36,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
@@ -113,6 +116,33 @@ describe('PromQueryField typeahead handling', () => {
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label value suggestions inside a label value context after a negated matching operator', () => {
const instance = shallow(
<PromQueryField
{...defaultProps}
labelKeys={{ '{}': ['label'] }}
labelValues={{ '{}': { label: ['a', 'b', 'c'] } }}
/>
).instance() as PromQueryField;
const value = Plain.deserialize('{label!=}');
const range = value.selection.merge({ anchorOffset: 8 });
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '!=',
prefix: '',
wrapperClasses: ['context-labels'],
labelKey: 'label',
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
expect(result.suggestions).toEqual([
{
items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
label: 'Label values for "label"',
},
]);
});
it('returns a refresher on label context and unavailable metric', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />

View File

@@ -111,7 +111,7 @@ export function willApplySuggestion(
case 'context-label-values': {
// Always add quotes and remove existing ones instead
if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) {
if (!typeaheadText.match(/^(!?=~?"|")/)) {
suggestion = `"${suggestion}`;
}
if (getNextCharacter() !== '"') {
@@ -421,7 +421,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
const containsMetric = selector.indexOf('__name__=') > -1;
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
// Label values
if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
const labelValues = this.state.labelValues[selector][labelKey];

View File

@@ -228,7 +228,13 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
const offset = range.startOffset;
const text = selection.anchorNode.textContent;
let prefix = text.substr(0, offset);
if (cleanText) {
// Label values could have valid characters erased if `cleanText()` is
// blindly applied, which would undesirably interfere with suggestions
const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/);
if (labelValueMatch) {
prefix = labelValueMatch[1];
} else if (cleanText) {
prefix = cleanText(prefix);
}

View File

@@ -28,7 +28,7 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
const selectorRegexp = /\{[^}]*?\}/;
const labelRegexp = /\b\w+="[^"\n]*?"/g;
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
if (!query.match(selectorRegexp)) {
// Special matcher for metrics
@@ -66,11 +66,8 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
// Extract clean labels to form clean selector, incomplete labels are dropped
const selector = query.slice(prefixOpen, suffixClose);
const labels = {};
selector.replace(labelRegexp, match => {
const delimiterIndex = match.indexOf('=');
const key = match.slice(0, delimiterIndex);
const value = match.slice(delimiterIndex + 1, match.length);
labels[key] = value;
selector.replace(labelRegexp, (_, key, operator, value) => {
labels[key] = { value, operator };
return '';
});
@@ -78,12 +75,12 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
const metricPrefix = query.slice(0, prefixOpen);
const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/);
if (metricMatch) {
labels['__name__'] = `"${metricMatch[0]}"`;
labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' };
}
// Build sorted selector
const labelKeys = Object.keys(labels).sort();
const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
const cleanSelector = labelKeys.map(key => `${key}${labels[key].operator}${labels[key].value}`).join(',');
const selectorString = ['{', cleanSelector, '}'].join('');

View File

@@ -1,17 +1,21 @@
import _ from 'lodash';
export class CloudWatchConfigCtrl {
static templateUrl = 'partials/config.html';
current: any;
datasourceSrv: any;
accessKeyExist = false;
secretKeyExist = false;
/** @ngInject */
constructor($scope) {
constructor($scope, datasourceSrv) {
this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
this.current.jsonData.authType = this.current.jsonData.authType || 'credentials';
this.accessKeyExist = this.current.secureJsonFields.accessKey;
this.secretKeyExist = this.current.secureJsonFields.secretKey;
this.datasourceSrv = datasourceSrv;
this.getRegions();
}
resetAccessKey() {
@@ -36,4 +40,47 @@ export class CloudWatchConfigCtrl {
{ name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' },
{ name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' },
];
regions = [
'ap-northeast-1',
'ap-northeast-2',
'ap-northeast-3',
'ap-south-1',
'ap-southeast-1',
'ap-southeast-2',
'ca-central-1',
'cn-north-1',
'cn-northwest-1',
'eu-central-1',
'eu-north-1',
'eu-west-1',
'eu-west-2',
'eu-west-3',
'me-south-1',
'sa-east-1',
'us-east-1',
'us-east-2',
'us-gov-east-1',
'us-gov-west-1',
'us-iso-east-1',
'us-isob-east-1',
'us-west-1',
'us-west-2',
];
getRegions() {
this.datasourceSrv
.loadDatasource(this.current.name)
.then(ds => {
return ds.getRegions();
})
.then(
regions => {
this.regions = _.map(regions, 'value');
},
err => {
console.error('failed to get latest regions');
}
);
}
}

View File

@@ -39,7 +39,7 @@
<div class="gf-form">
<label class="gf-form-label width-13">Default Region</label>
<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2', 'us-isob-east-1', 'us-iso-east-1']"></select>
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ctrl.regions"></select>
<info-popover mode="right-absolute">
Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
</info-popover>

View File

@@ -20,7 +20,7 @@ export class PostgresDatasource {
this.interval = (instanceSettings.jsonData || {}).timeInterval;
}
interpolateVariable(value, variable) {
interpolateVariable = (value, variable) => {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
return this.queryModel.quoteLiteral(value);
@@ -37,7 +37,7 @@ export class PostgresDatasource {
return this.queryModel.quoteLiteral(v);
});
return quotedValues.join(',');
}
};
query(options) {
const queries = _.filter(options.targets, target => {