mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into datasource-dashboards-to-react
This commit is contained in:
@@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl {
|
||||
defaults: any = {
|
||||
type: 'email',
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '15m',
|
||||
settings: {
|
||||
httpMethod: 'POST',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'] }} />
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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('');
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user