Commit 02769b6d by Peter Holmberg

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

parents 84bbfe11 1d60c45a
......@@ -4,16 +4,22 @@
* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
### Minor
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
### Breaking changes
* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
# 5.3.2 (unreleased)
* **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
# 5.3.1 (2018-10-16)
* **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616)
......
......@@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
### Dependencies
- Go 1.11
- Go (Latest Stable)
- NodeJS LTS
### Building the backend
......
# Guide to Upgrading Dependencies
Upgrading Go or Node.js requires making changes in many different files. See below for a list and explanation for each.
## Go
- CircleCi
- `grafana/build-container`
- Appveyor
- Dockerfile
## Node.js
- CircleCI
- `grafana/build-container`
- Appveyor
- Dockerfile
## Go Dependencies
Updated using `dep`.
- `Gopkg.toml`
- `Gopkg.lock`
## Node.js Dependencies
Updated using `yarn`.
- `package.json`
## Where to make changes
### CircleCI
Our builds run on CircleCI through our build script.
#### Files
- `.circleci/config.yml`.
#### Dependencies
- nodejs
- golang
- grafana/build-container (our custom docker build container)
### grafana/build-container
The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the neccesary dependencies.
Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
#### Dependencies
- fpm
- nodejs
- golang
- crosscompiling (several compilers)
### Appveyor
Master and release builds trigger test runs on Appveyors build environment so that tests will run on Windows.
#### Files:
- `appveyor.yml`
#### Dependencies
- nodejs
- golang
### Dockerfile
There is a Docker build for Grafana in the root of the project that allows anyone to build Grafana just using Docker.
#### Files
- `Dockerfile`
#### Dependencies
- nodejs
- golang
### Local developer environments
Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.
\ No newline at end of file
......@@ -46,7 +46,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
## IAM Policies
Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
and EC2 tags/instances. You can attach these permissions to IAM roles and
and EC2 tags/instances/regions. You can attach these permissions to IAM roles and
utilize Grafana's built-in support for assuming roles.
Here is a minimal policy example:
......@@ -65,11 +65,12 @@ Here is a minimal policy example:
"Resource": "*"
},
{
"Sid": "AllowReadingTagsFromEC2",
"Sid": "AllowReadingTagsInstancesRegionsFromEC2",
"Effect": "Allow",
"Action": [
"ec2:DescribeTags",
"ec2:DescribeInstances"
"ec2:DescribeInstances",
"ec2:DescribeRegions"
],
"Resource": "*"
}
......
......@@ -87,7 +87,7 @@ docker run \
## Building a custom Grafana image with pre-installed plugins
In the [grafana-docker](https://github.com/grafana/grafana-docker/) there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image. It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
In the [grafana-docker](https://github.com/grafana/grafana/tree/master/packaging/docker) there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image. It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
Example of how to build and run:
```bash
......@@ -103,6 +103,21 @@ docker run \
grafana:latest-with-plugins
```
## Installing Plugins from other sources
> Only available in Grafana v5.3.1+
It's possible to install plugins from custom url:s by specifying the url like this: `GF_INSTALL_PLUGINS=<url to plugin zip>;<plugin name>`
```bash
docker run \
-d \
-p 3000:3000 \
--name=grafana \
-e "GF_INSTALL_PLUGINS=http://plugin-domain.com/my-custom-plugin.zip;custom-plugin" \
grafana/grafana
```
## Configuring AWS Credentials for CloudWatch Support
```bash
......
......@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
## Dependencies
- [Go 1.11](https://golang.org/dl/)
- [Go (Latest Stable)](https://golang.org/dl/)
- [Git](https://git-scm.com/downloads)
- [NodeJS LTS](https://nodejs.org/download/)
- node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
......
......@@ -17,24 +17,8 @@ func GetDataSources(c *m.ReqContext) Response {
return Error(500, "Failed to query datasources", err)
}
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
User: c.SignedInUser,
Datasources: query.Result,
}
var datasources []*m.DataSource
if err := bus.Dispatch(&dsFilterQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return Error(500, "Could not get datasources", err)
}
datasources = query.Result
} else {
datasources = dsFilterQuery.Result
}
result := make(dtos.DataSourceList, 0)
for _, ds := range datasources {
for _, ds := range query.Result {
dsItem := dtos.DataSourceListItemDTO{
OrgId: ds.OrgId,
Id: ds.Id,
......
......@@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string {
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
return &AlertNotification{
Id: notification.Id,
Name: notification.Name,
Type: notification.Type,
IsDefault: notification.IsDefault,
Created: notification.Created,
Updated: notification.Updated,
Frequency: formatShort(notification.Frequency),
SendReminder: notification.SendReminder,
Settings: notification.Settings,
Id: notification.Id,
Name: notification.Name,
Type: notification.Type,
IsDefault: notification.IsDefault,
Created: notification.Created,
Updated: notification.Updated,
Frequency: formatShort(notification.Frequency),
SendReminder: notification.SendReminder,
DisableResolveMessage: notification.DisableResolveMessage,
Settings: notification.Settings,
}
}
type AlertNotification struct {
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
IsDefault bool `json:"isDefault"`
SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Settings *simplejson.Json `json:"settings"`
Id int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
IsDefault bool `json:"isDefault"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency string `json:"frequency"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Settings *simplejson.Json `json:"settings"`
}
type AlertTestCommand struct {
......@@ -100,11 +102,12 @@ type EvalMatch struct {
}
type NotificationTestCommand struct {
Name string `json:"name"`
Type string `json:"type"`
SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"`
Settings *simplejson.Json `json:"settings"`
Name string `json:"name"`
Type string `json:"type"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency string `json:"frequency"`
Settings *simplejson.Json `json:"settings"`
}
type PauseAlertCommand struct {
......
......@@ -23,38 +23,41 @@ var (
)
type AlertNotification struct {
Id int64 `json:"id"`
OrgId int64 `json:"-"`
Name string `json:"name"`
Type string `json:"type"`
SendReminder bool `json:"sendReminder"`
Frequency time.Duration `json:"frequency"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Id int64 `json:"id"`
OrgId int64 `json:"-"`
Name string `json:"name"`
Type string `json:"type"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency time.Duration `json:"frequency"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
type CreateAlertNotificationCommand struct {
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency string `json:"frequency"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings"`
OrgId int64 `json:"-"`
Result *AlertNotification
}
type UpdateAlertNotificationCommand struct {
Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
SendReminder bool `json:"sendReminder"`
Frequency string `json:"frequency"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings" binding:"Required"`
Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
SendReminder bool `json:"sendReminder"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Frequency string `json:"frequency"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings" binding:"Required"`
OrgId int64 `json:"-"`
Result *AlertNotification
......
......@@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct {
type DsPermissionType int
const (
DsPermissionQuery DsPermissionType = 1 << iota
DsPermissionNoAccess
DsPermissionNoAccess DsPermissionType = iota
DsPermissionQuery
)
func (p DsPermissionType) String() string {
......@@ -207,12 +207,6 @@ func (p DsPermissionType) String() string {
return names[int(p)]
}
type HasRequiredDataSourcePermissionQuery struct {
Id int64
User *SignedInUser
RequiredPermission DsPermissionType
}
type GetDataSourcePermissionsForUserQuery struct {
User *SignedInUser
Result map[int64]DsPermissionType
......
......@@ -27,6 +27,7 @@ type Notifier interface {
GetNotifierId() int64
GetIsDefault() bool
GetSendReminder() bool
GetDisableResolveMessage() bool
GetFrequency() time.Duration
}
......
......@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
......@@ -15,13 +14,14 @@ const (
)
type NotifierBase struct {
Name string
Type string
Id int64
IsDeault bool
UploadImage bool
SendReminder bool
Frequency time.Duration
Name string
Type string
Id int64
IsDeault bool
UploadImage bool
SendReminder bool
DisableResolveMessage bool
Frequency time.Duration
log log.Logger
}
......@@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
}
return NotifierBase{
Id: model.Id,
Name: model.Name,
IsDeault: model.IsDefault,
Type: model.Type,
UploadImage: uploadImage,
SendReminder: model.SendReminder,
Frequency: model.Frequency,
log: log.New("alerting.notifier." + model.Name),
Id: model.Id,
Name: model.Name,
IsDeault: model.IsDefault,
Type: model.Type,
UploadImage: uploadImage,
SendReminder: model.SendReminder,
DisableResolveMessage: model.DisableResolveMessage,
Frequency: model.Frequency,
log: log.New("alerting.notifier." + model.Name),
}
}
......@@ -83,6 +84,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
}
}
// Do not notify when state is OK if DisableResolveMessage is set to true
if context.Rule.State == models.AlertStateOK && n.DisableResolveMessage {
return false
}
return true
}
......@@ -106,6 +112,10 @@ func (n *NotifierBase) GetSendReminder() bool {
return n.SendReminder
}
func (n *NotifierBase) GetDisableResolveMessage() bool {
return n.DisableResolveMessage
}
func (n *NotifierBase) GetFrequency() time.Duration {
return n.Frequency
}
......@@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) {
base := NewNotifierBase(model)
So(base.UploadImage, ShouldBeTrue)
})
Convey("default value should be false for backwards compatibility", func() {
base := NewNotifierBase(model)
So(base.DisableResolveMessage, ShouldBeFalse)
})
})
}
......@@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
alert_notification.updated,
alert_notification.settings,
alert_notification.is_default,
alert_notification.disable_resolve_message,
alert_notification.send_reminder,
alert_notification.frequency
FROM alert_notification
......@@ -106,6 +107,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
alert_notification.updated,
alert_notification.settings,
alert_notification.is_default,
alert_notification.disable_resolve_message,
alert_notification.send_reminder,
alert_notification.frequency
FROM alert_notification
......@@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
}
alertNotification := &m.AlertNotification{
OrgId: cmd.OrgId,
Name: cmd.Name,
Type: cmd.Type,
Settings: cmd.Settings,
SendReminder: cmd.SendReminder,
Frequency: frequency,
Created: time.Now(),
Updated: time.Now(),
IsDefault: cmd.IsDefault,
OrgId: cmd.OrgId,
Name: cmd.Name,
Type: cmd.Type,
Settings: cmd.Settings,
SendReminder: cmd.SendReminder,
DisableResolveMessage: cmd.DisableResolveMessage,
Frequency: frequency,
Created: time.Now(),
Updated: time.Now(),
IsDefault: cmd.IsDefault,
}
if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
......@@ -210,6 +213,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
current.Type = cmd.Type
current.IsDefault = cmd.IsDefault
current.SendReminder = cmd.SendReminder
current.DisableResolveMessage = cmd.DisableResolveMessage
if current.SendReminder {
if cmd.Frequency == "" {
......@@ -224,7 +228,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
current.Frequency = frequency
}
sess.UseBool("is_default", "send_reminder")
sess.UseBool("is_default", "send_reminder", "disable_resolve_message")
if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
return err
......
......@@ -219,6 +219,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
So(cmd.Result.OrgId, ShouldNotEqual, 0)
So(cmd.Result.Type, ShouldEqual, "email")
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
Convey("Cannot save Alert Notification with the same name", func() {
err = CreateAlertNotificationCommand(cmd)
......@@ -227,18 +228,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
Convey("Can update alert notification", func() {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: cmd.Result.Id,
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: cmd.Result.Id,
}
err := UpdateAlertNotification(newCmd)
So(err, ShouldBeNil)
So(newCmd.Result.Name, ShouldEqual, "NewName")
So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
So(newCmd.Result.DisableResolveMessage, ShouldBeTrue)
})
Convey("Can update alert notification to disable sending of reminders", func() {
......
......@@ -71,6 +71,9 @@ func addAlertMigrations(mg *Migrator) {
mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
}))
mg.AddMigration("Add column disable_resolve_message", NewAddColumnMigration(alert_notification, &Column{
Name: "disable_resolve_message", Type: DB_Bool, Nullable: false, Default: "0",
}))
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
......
......@@ -234,10 +234,37 @@ func parseMultiSelectValue(input string) []string {
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
regions := []string{
"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",
"ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-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-west-1", "us-west-2",
"cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1",
}
err := e.ensureClientSession("us-east-1")
if err != nil {
return nil, err
}
r, err := e.ec2Svc.DescribeRegions(&ec2.DescribeRegionsInput{})
if err != nil {
// ignore error for backward compatibility
plog.Error("Failed to get regions", "error", err)
} else {
for _, region := range r.Regions {
exists := false
for _, existingRegion := range regions {
if existingRegion == *region.RegionName {
exists = true
break
}
}
if !exists {
regions = append(regions, *region.RegionName)
}
}
}
sort.Strings(regions)
result := make([]suggestData, 0)
for _, region := range regions {
result = append(result, suggestData{Text: region, Value: region})
......
......@@ -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('');
......
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 => {
......
......@@ -50,19 +50,22 @@
function checkIsReady() {
var panelsRendered = page.evaluate(function() {
var panelCount = document.querySelectorAll('.panel').length;
var panelCount = document.querySelectorAll('plugin-component').length;
return window.panelsRendered >= panelCount;
});
if (panelsRendered || totalWaitMs > timeoutMs) {
var bb = page.evaluate(function () {
return document.getElementsByClassName("main-view")[0].getBoundingClientRect();
var container = document.getElementsByClassName("dashboard-container")
if (container.length == 0) {
container = document.getElementsByClassName("panel-container")
}
return container[0].getBoundingClientRect();
});
page.clipRect = {
top: bb.top,
left: bb.left,
width: bb.width,
// reset viewport to render full page
page.viewportSize = {
width: bb.width,
height: bb.height
};
......
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