Merge branch 'master' into react-panels-step1

This commit is contained in:
Torkel Ödegaard
2018-10-19 09:15:37 +02:00
99 changed files with 1500 additions and 2252 deletions
+1
View File
@@ -4,6 +4,7 @@ init_cmds = [
["./bin/grafana-server", "cfg:app_mode=development"]
]
watch_all = true
follow_symlinks = true
watch_dirs = [
"$WORKDIR/pkg",
"$WORKDIR/public/views",
+1
View File
@@ -170,6 +170,7 @@ jobs:
- scripts/*.sh
- scripts/publish
- scripts/build/release_publisher/release_publisher
- scripts/build/publish.sh
build:
docker:
+18 -3
View File
@@ -2,22 +2,37 @@
### New Features
* **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)
* **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)
* **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
### Minor
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
* **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.1 (unreleased)
# 5.3.2 (unreleased)
* **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
* **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda)
# 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)
* **Stackdriver**: Filter option disappears after removing initial filter [#13607](https://github.com/grafana/grafana/issues/13607)
* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
* **InfluxDB**: Fix for annotation issue that caused text to be shown twice [#13553](https://github.com/grafana/grafana/issues/13553)
* **Variables**: Fix nesting variables leads to exception and missing refresh [#13628](https://github.com/grafana/grafana/issues/13628)
* **Variables**: Prometheus: Single letter labels are not supported [#13641](https://github.com/grafana/grafana/issues/13641), thx [@olshansky](https://github.com/olshansky)
* **Graph**: Fix graph time formatting for Last 24h ranges [#13650](https://github.com/grafana/grafana/issues/13650)
* **Playlist**: Fix cannot add dashboards with long names to playlist [#13464](https://github.com/grafana/grafana/issues/13464), thx [@neufeldtech](https://github.com/neufeldtech)
* **HTTP API**: Fix /api/org/users so that query and limit querystrings works
# 5.3.0 (2018-10-10)
+16 -16
View File
@@ -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
@@ -69,15 +69,27 @@ bra run
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
### Building a docker image (on linux/amd64)
### Building a Docker image
This builds a docker image from your local sources:
There are two different ways to build a Grafana docker image. If you're machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev`
#### Building on linux/amd64 (fast)
1. Build the frontend `go run build.go build-frontend`
2. Build the docker image `make build-docker-dev`
The resulting image will be tagged as `grafana/grafana:dev`
#### Building anywhere (slower)
Choose this option to build on platforms other than linux/amd64 and/or not have to setup the Grafana development environment.
1. `make build-docker-full` or `docker build -t grafana/grafana:dev .`
The resulting image will be tagged as `grafana/grafana:dev`
### Dev config
Create a custom.ini in the conf directory to override default configuration options.
@@ -113,18 +125,6 @@ GRAFANA_TEST_DB=mysql go test ./pkg/...
GRAFANA_TEST_DB=postgres go test ./pkg/...
```
## Building custom docker image
You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself.
```bash
git clone https://github.com/grafana/grafana
cd grafana
docker build -t grafana:dev .
docker run -d --name=grafana -p 3000:3000 grafana:dev
```
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
## Contribute
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
@@ -138,5 +138,5 @@ plugin development.
## License
Grafana is distributed under Apache 2.0 License.
Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE.md).
+89
View File
@@ -0,0 +1,89 @@
# 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.
+1 -1
View File
@@ -5,7 +5,7 @@ os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "6"
nodejs_version: "8"
GOPATH: C:\gopath
GOVERSION: 1.11
+4 -3
View File
@@ -156,9 +156,9 @@ Since not all datasources have the same configuration settings we only have the
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version |
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) |
| timeField | string | Elastic | Which field that should be used as timestamp |
| interval | string | Elastic | Index date time format |
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) |
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
| interval | string | Elasticsearch | Index date time format |
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
| assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
| defaultRegion | string | Cloudwatch | AWS region |
@@ -166,6 +166,7 @@ Since not all datasources have the same configuration settings we only have the
| tsdbVersion | string | OpenTSDB | Version |
| tsdbResolution | string | OpenTSDB | Resolution |
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
| encrypt | string | MSSQL | Connection SSL encryption handling. 'disable', 'false' or 'true' |
| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 |
| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
| maxOpenConns | number | MySQL, PostgreSQL & MSSQL | Maximum number of open connections to the database (Grafana v5.4+) |
+1 -1
View File
@@ -128,7 +128,7 @@ Example json body:
In DingTalk PC Client:
1. Click "more" icon on left bottom of the panel.
1. Click "more" icon on upper right of the panel.
2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage".
@@ -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": "*"
}
+3 -2
View File
@@ -32,6 +32,7 @@ Name | Description
*Database* | Name of your MSSQL database.
*User* | Database user's login/username
*Password* | Database user's password
*Encrypt* | This option determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server, default `false` (Grafana v5.4+).
*Max open* | The maximum number of open connections to the database, default `unlimited` (Grafana v5.4+).
*Max idle* | The maximum number of connections in the idle connection pool, default `2` (Grafana v5.4+).
*Max lifetime* | The maximum amount of time in seconds a connection may be reused, default `14400`/4 hours (Grafana v5.4+).
@@ -72,8 +73,8 @@ Make sure the user does not get any unwanted privileges from the public role.
### Known Issues
MSSQL 2008 and 2008 R2 engine cannot handle login records when SSL encryption is not disabled. Due to this you may receive an `Login error: EOF` error when trying to create your datasource.
To fix MSSQL 2008 R2 issue, install MSSQL 2008 R2 Service Pack 2. To fix MSSQL 2008 issue, install Microsoft MSSQL 2008 Service Pack 3 and Cumulative update package 3 for MSSQL 2008 SP3.
If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect.
If possible, we recommend you to use the latest service pack available for optimal compatibility.
## Query Editor
+16 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "5.3.0",
"testing": "5.3.0"
"stable": "5.3.1",
"testing": "5.3.1"
}
+1
View File
@@ -160,6 +160,7 @@
"react-redux": "^5.0.7",
"react-select": "2.1.0",
"react-sizeme": "^2.3.6",
"react-table": "^6.8.6",
"react-transition-group": "^2.2.1",
"redux": "^4.0.0",
"redux-logger": "^3.0.6",
+3 -3
View File
@@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() {
datasourceRoute.Get("/", Wrap(GetDataSources))
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
}, reqOrgAdmin)
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
apiRoute.Get("/plugins", Wrap(GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"os"
"path"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -251,8 +252,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
return Error(403, err.Error(), err)
}
if err == m.ErrDashboardContainsInvalidAlertData {
return Error(500, "Invalid alert data. Cannot save dashboard", err)
if validationErr, ok := err.(alerting.ValidationError); ok {
return Error(422, validationErr.Error(), nil)
}
if err != nil {
+2 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
@@ -725,7 +726,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
+18 -1
View File
@@ -2,6 +2,7 @@ package api
import (
"fmt"
"github.com/pkg/errors"
"time"
"github.com/grafana/grafana/pkg/api/pluginproxy"
@@ -14,6 +15,20 @@ import (
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
User: c.SignedInUser,
}
if err := bus.Dispatch(&userPermissionsQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
} else {
permissionType, exists := userPermissionsQuery.Result[id]
if exists && permissionType != m.DsPermissionQuery {
return nil, errors.New("User not allowed to access datasource")
}
}
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
cacheKey := fmt.Sprintf("ds-%d", id)
@@ -38,7 +53,9 @@ func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.Data
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c)
dsId := c.ParamsInt64(":id")
ds, err := hs.getDatasourceFromCache(dsId, c)
if err != nil {
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
+7 -7
View File
@@ -20,8 +20,8 @@ func GetDataSources(c *m.ReqContext) Response {
result := make(dtos.DataSourceList, 0)
for _, ds := range query.Result {
dsItem := dtos.DataSourceListItemDTO{
Id: ds.Id,
OrgId: ds.OrgId,
Id: ds.Id,
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
@@ -49,7 +49,7 @@ func GetDataSources(c *m.ReqContext) Response {
return JSON(200, &result)
}
func GetDataSourceByID(c *m.ReqContext) Response {
func GetDataSourceById(c *m.ReqContext) Response {
query := m.GetDataSourceByIdQuery{
Id: c.ParamsInt64(":id"),
OrgId: c.OrgId,
@@ -68,14 +68,14 @@ func GetDataSourceByID(c *m.ReqContext) Response {
return JSON(200, &dtos)
}
func DeleteDataSourceByID(c *m.ReqContext) Response {
func DeleteDataSourceById(c *m.ReqContext) Response {
id := c.ParamsInt64(":id")
if id <= 0 {
return Error(400, "Missing valid datasource id", nil)
}
ds, err := getRawDataSourceByID(id, c.OrgId)
ds, err := getRawDataSourceById(id, c.OrgId)
if err != nil {
return Error(400, "Failed to delete datasource", nil)
}
@@ -186,7 +186,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
return nil
}
ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
if err != nil {
return err
}
@@ -206,7 +206,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
return nil
}
func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
func getRawDataSourceById(id int64, orgID int64) (*m.DataSource, error) {
query := m.GetDataSourceByIdQuery{
Id: id,
OrgId: orgID,
@@ -236,7 +236,7 @@ func GetDataSourceByName(c *m.ReqContext) Response {
}
// Get /api/datasources/id/:name
func GetDataSourceIDByName(c *m.ReqContext) Response {
func GetDataSourceIdByName(c *m.ReqContext) Response {
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
+26 -23
View File
@@ -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 {
+14 -1
View File
@@ -22,7 +22,20 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
return nil, err
}
orgDataSources = query.Result
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
User: c.SignedInUser,
Datasources: query.Result,
}
if err := bus.Dispatch(&dsFilterQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
orgDataSources = query.Result
} else {
orgDataSources = dsFilterQuery.Result
}
}
datasources := make(map[string]interface{})
+1 -1
View File
@@ -100,7 +100,7 @@ func listenToSystemSignals(server *GrafanaServerImpl) {
sighupChan := make(chan os.Signal, 1)
signal.Notify(sighupChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
for {
select {
+3 -1
View File
@@ -185,7 +185,9 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
if ldapUser.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false {
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
}
}
}
+26 -23
View File
@@ -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
-1
View File
@@ -21,7 +21,6 @@ var (
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
+29 -2
View File
@@ -30,6 +30,7 @@ var (
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.")
ErrDataSourceAccessDenied = errors.New("Data source access denied")
)
type DsAccess string
@@ -167,6 +168,7 @@ type DeleteDataSourceByNameCommand struct {
type GetDataSourcesQuery struct {
OrgId int64
User *SignedInUser
Result []*DataSource
}
@@ -187,6 +189,31 @@ type GetDataSourceByNameQuery struct {
}
// ---------------------
// EVENTS
type DataSourceCreatedEvent struct {
// Permissions
// ---------------------
type DsPermissionType int
const (
DsPermissionNoAccess DsPermissionType = iota
DsPermissionQuery
)
func (p DsPermissionType) String() string {
names := map[int]string{
int(DsPermissionQuery): "Query",
int(DsPermissionNoAccess): "No Access",
}
return names[int(p)]
}
type GetDataSourcePermissionsForUserQuery struct {
User *SignedInUser
Result map[int64]DsPermissionType
}
type DatasourcesPermissionFilterQuery struct {
User *SignedInUser
Datasources []*DataSource
Result []*DataSource
}
@@ -2,6 +2,7 @@ package conditions
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -31,12 +32,12 @@ type ThresholdEvaluator struct {
func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvaluator, error) {
params := model.Get("params").MustArray()
if len(params) == 0 {
return nil, alerting.ValidationError{Reason: "Evaluator missing threshold parameter"}
return nil, fmt.Errorf("Evaluator missing threshold parameter")
}
firstParam, ok := params[0].(json.Number)
if !ok {
return nil, alerting.ValidationError{Reason: "Evaluator has invalid parameter"}
return nil, fmt.Errorf("Evaluator has invalid parameter")
}
defaultEval := &ThresholdEvaluator{Type: typ}
@@ -107,7 +108,7 @@ func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
typ := model.Get("type").MustString()
if typ == "" {
return nil, alerting.ValidationError{Reason: "Evaluator missing type property"}
return nil, fmt.Errorf("Evaluator missing type property")
}
if inSlice(typ, defaultTypes) {
@@ -122,7 +123,7 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
return &NoValueEvaluator{}, nil
}
return nil, alerting.ValidationError{Reason: "Evaluator invalid evaluator type: " + typ}
return nil, fmt.Errorf("Evaluator invalid evaluator type: %s", typ)
}
func inSlice(a string, list []string) bool {
+5 -6
View File
@@ -82,8 +82,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
if collapsed && collapsedJSON.MustBool() {
// extract alerts from sub panels for collapsed panels
alertSlice, err := e.getAlertFromPanels(panel,
validateAlertFunc)
alertSlice, err := e.getAlertFromPanels(panel, validateAlertFunc)
if err != nil {
return nil, err
}
@@ -100,7 +99,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
panelID, err := panel.Get("id").Int64()
if err != nil {
return nil, fmt.Errorf("panel id is required. err %v", err)
return nil, ValidationError{Reason: "A numeric panel id property is missing"}
}
// backward compatibility check, can be removed later
@@ -146,7 +145,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
datasource, err := e.lookupDatasourceID(dsName)
if err != nil {
return nil, err
e.log.Debug("Error looking up datasource", "error", err)
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
}
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
@@ -167,8 +167,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
}
if !validateAlertFunc(alert) {
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
return nil, m.ErrDashboardContainsInvalidAlertData
return nil, ValidationError{Reason: fmt.Sprintf("Panel id is not correct, alertName=%v, panelId=%v", alert.Name, alert.PanelId)}
}
alerts = append(alerts, alert)
+1 -1
View File
@@ -258,7 +258,7 @@ func TestAlertRuleExtraction(t *testing.T) {
Convey("Should fail on save", func() {
_, err := extractor.GetAlerts()
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
})
})
})
+1
View File
@@ -27,6 +27,7 @@ type Notifier interface {
GetNotifierId() int64
GetIsDefault() bool
GetSendReminder() bool
GetDisableResolveMessage() bool
GetFrequency() time.Duration
}
+26 -16
View File
@@ -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)
})
})
}
+5 -5
View File
@@ -36,13 +36,13 @@ type ValidationError struct {
}
func (e ValidationError) Error() string {
extraInfo := ""
extraInfo := e.Reason
if e.Alertid != 0 {
extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid)
}
if e.PanelId != 0 {
extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId)
extraInfo = fmt.Sprintf("%s PanelId: %v", extraInfo, e.PanelId)
}
if e.DashboardId != 0 {
@@ -50,10 +50,10 @@ func (e ValidationError) Error() string {
}
if e.Err != nil {
return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo)
return fmt.Sprintf("Alert validation error: %s%s", e.Err.Error(), extraInfo)
}
return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo)
return fmt.Sprintf("Alert validation error: %s", extraInfo)
}
var (
@@ -128,7 +128,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
}
if len(model.Conditions) == 0 {
return nil, fmt.Errorf("Alert is missing conditions")
return nil, ValidationError{Reason: "Alert is missing conditions"}
}
return model, nil
+1 -1
View File
@@ -73,7 +73,7 @@ func (srv *CleanUpService) cleanUpTmpFiles() {
}
}
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "kept", len(files))
}
func (srv *CleanUpService) shouldCleanupTempFile(filemtime time.Time, now time.Time) bool {
+6 -2
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
@@ -25,7 +26,9 @@ type DashboardProvisioningService interface {
// NewService factory for creating a new dashboard service
var NewService = func() DashboardService {
return &dashboardServiceImpl{}
return &dashboardServiceImpl{
log: log.New("dashboard-service"),
}
}
// NewProvisioningService factory for creating a new dashboard provisioning service
@@ -45,6 +48,7 @@ type SaveDashboardDTO struct {
type dashboardServiceImpl struct {
orgId int64
user *models.SignedInUser
log log.Logger
}
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
@@ -89,7 +93,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
return nil, models.ErrDashboardContainsInvalidAlertData
return nil, err
}
}
@@ -117,12 +117,12 @@ func TestDashboardService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return errors.New("error")
return errors.New("Alert validation error")
})
dto.Dashboard = models.NewDashboard("Dash")
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData)
So(err.Error(), ShouldEqual, "Alert validation error")
})
})
+14 -10
View File
@@ -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
@@ -44,8 +44,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
@@ -100,8 +100,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.NotifierId,
Version: s.Version,
Id: s.NotifierId,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
@@ -111,8 +111,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
Convey("Updating existing state to pending with incorrect version since alert rule state update version is higher", func() {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: 1000,
}
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
@@ -125,8 +125,8 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
@@ -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() {
+1
View File
@@ -27,6 +27,7 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&datasource)
if err != nil {
return err
}
@@ -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]))
+9 -2
View File
@@ -53,6 +53,7 @@ type SqlStore struct {
dbCfg DatabaseConfig
engine *xorm.Engine
log log.Logger
Dialect migrator.Dialect
skipEnsureAdmin bool
}
@@ -125,10 +126,12 @@ func (ss *SqlStore) Init() error {
}
ss.engine = engine
ss.Dialect = migrator.NewDialect(ss.engine)
// temporarily still set global var
x = engine
dialect = migrator.NewDialect(x)
dialect = ss.Dialect
migrator := migrator.NewMigrator(x)
migrations.AddMigrations(migrator)
@@ -347,7 +350,11 @@ func InitTestDB(t *testing.T) *SqlStore {
t.Fatalf("Failed to init test database: %v", err)
}
dialect = migrator.NewDialect(engine)
sqlstore.Dialect = migrator.NewDialect(engine)
// temp global var until we get rid of global vars
dialect = sqlstore.Dialect
if err := dialect.CleanDB(); err != nil {
t.Fatalf("Failed to clean test db %v", err)
}
+16 -10
View File
@@ -86,9 +86,10 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
}
func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
result := &tsdb.Response{
results := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult),
}
resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries))
eg, ectx := errgroup.WithContext(ctx)
@@ -102,10 +103,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
RefId := queryContext.Queries[i].RefId
query, err := parseQuery(queryContext.Queries[i].Model)
if err != nil {
result.Results[RefId] = &tsdb.QueryResult{
results.Results[RefId] = &tsdb.QueryResult{
Error: err,
}
return result, nil
return results, nil
}
query.RefId = RefId
@@ -118,10 +119,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
}
if query.Id == "" && query.Expression != "" {
result.Results[query.RefId] = &tsdb.QueryResult{
results.Results[query.RefId] = &tsdb.QueryResult{
Error: fmt.Errorf("Invalid query: id should be set if using expression"),
}
return result, nil
return results, nil
}
eg.Go(func() error {
@@ -130,12 +131,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
return err
}
if err != nil {
result.Results[query.RefId] = &tsdb.QueryResult{
resultChan <- &tsdb.QueryResult{
RefId: query.RefId,
Error: err,
}
return nil
}
result.Results[queryRes.RefId] = queryRes
resultChan <- queryRes
return nil
})
}
@@ -149,10 +151,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
return err
}
for _, queryRes := range queryResponses {
result.Results[queryRes.RefId] = queryRes
if err != nil {
result.Results[queryRes.RefId].Error = err
queryRes.Error = err
}
resultChan <- queryRes
}
return nil
})
@@ -162,8 +164,12 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
if err := eg.Wait(); err != nil {
return nil, err
}
close(resultChan)
for result := range resultChan {
results.Results[result.RefId] = result
}
return result, nil
return results, nil
}
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
+29 -2
View File
@@ -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})
+6 -1
View File
@@ -52,13 +52,18 @@ func generateConnectionString(datasource *models.DataSource) string {
}
server, port := hostParts[0], hostParts[1]
return fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
server,
port,
datasource.Database,
datasource.User,
password,
)
if encrypt != "false" {
connStr += fmt.Sprintf("encrypt=%s;", encrypt)
}
return connStr
}
type mssqlRowTransformer struct {
+6 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
@@ -20,10 +21,14 @@ func init() {
func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
logger := log.New("tsdb.mysql")
protocol := "tcp"
if strings.HasPrefix(datasource.Url, "/") {
protocol = "unix"
}
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
datasource.User,
datasource.Password,
"tcp",
protocol,
datasource.Url,
datasource.Database,
)
@@ -18,6 +18,10 @@ export interface Props {
}
class AddPermissions extends Component<Props, NewDashboardAclItem> {
static defaultProps = {
showPermissionLevels: true,
};
constructor(props) {
super(props);
this.state = this.getCleanState();
@@ -22,10 +22,6 @@ export interface Props {
const getSelectedOption = (optionsWithDesc, value) => optionsWithDesc.find(option => option.value === value);
class DescriptionPicker extends Component<Props, any> {
constructor(props) {
super(props);
}
render() {
const { optionsWithDesc, onSelected, disabled, className, value } = this.props;
const selectedOption = getSelectedOption(optionsWithDesc, value);
@@ -207,7 +207,7 @@ export class ManageDashboardsCtrl {
const template =
'<move-to-folder-modal dismiss="dismiss()" ' +
'dashboards="model.dashboards" after-save="model.afterSave()">' +
'</move-to-folder-modal>`';
'</move-to-folder-modal>';
appEvents.emit('show-modal', {
templateHtml: template,
modalClass: 'modal--narrow',
@@ -17,7 +17,7 @@ export class SideMenu extends PureComponent {
render() {
return [
<div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
<img src="public/img/grafana_icon.svg" alt="graphana_logo" />
<img src="public/img/grafana_icon.svg" alt="Grafana" />
</div>,
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
<i className="fa fa-bars" />
@@ -8,7 +8,7 @@ Array [
onClick={[Function]}
>
<img
alt="graphana_logo"
alt="Grafana"
src="public/img/grafana_icon.svg"
/>
</div>,
+12
View File
@@ -104,5 +104,17 @@ describe('Directed acyclic graph', () => {
const actual = nodeH.getOptimizedInputEdges();
expect(actual).toHaveLength(0);
});
it('when linking non-existing input node with existing output node should throw error', () => {
expect(() => {
dag.link('non-existing', 'A');
}).toThrowError("cannot link input node named non-existing since it doesn't exist in graph");
});
it('when linking existing input node with non-existing output node should throw error', () => {
expect(() => {
dag.link('A', 'non-existing');
}).toThrowError("cannot link output node named non-existing since it doesn't exist in graph");
});
});
});
+18 -2
View File
@@ -15,6 +15,14 @@ export class Edge {
}
link(inputNode: Node, outputNode: Node) {
if (!inputNode) {
throw Error('inputNode is required');
}
if (!outputNode) {
throw Error('outputNode is required');
}
this.unlink();
this.inputNode = inputNode;
this.outputNode = outputNode;
@@ -152,7 +160,11 @@ export class Graph {
for (let n = 0; n < inputArr.length; n++) {
const i = inputArr[n];
if (typeof i === 'string') {
inputNodes.push(this.getNode(i));
const n = this.getNode(i);
if (!n) {
throw Error(`cannot link input node named ${i} since it doesn't exist in graph`);
}
inputNodes.push(n);
} else {
inputNodes.push(i);
}
@@ -161,7 +173,11 @@ export class Graph {
for (let n = 0; n < outputArr.length; n++) {
const i = outputArr[n];
if (typeof i === 'string') {
outputNodes.push(this.getNode(i));
const n = this.getNode(i);
if (!n) {
throw Error(`cannot link output node named ${i} since it doesn't exist in graph`);
}
outputNodes.push(n);
} else {
outputNodes.push(i);
}
@@ -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>
@@ -8,6 +8,7 @@ import { makeRegions, dedupAnnotations } from './events_processing';
export class AnnotationsSrv {
globalAnnotationsPromise: any;
alertStatesPromise: any;
datasourcePromises: any;
/** @ngInject */
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
@@ -18,6 +19,7 @@ export class AnnotationsSrv {
clearCache() {
this.globalAnnotationsPromise = null;
this.alertStatesPromise = null;
this.datasourcePromises = null;
}
getAnnotations(options) {
@@ -90,6 +92,7 @@ export class AnnotationsSrv {
const range = this.timeSrv.timeRange();
const promises = [];
const dsPromises = [];
for (const annotation of dashboard.annotations.list) {
if (!annotation.enable) {
@@ -99,10 +102,10 @@ export class AnnotationsSrv {
if (annotation.snapshotData) {
return this.translateQueryResult(annotation, annotation.snapshotData);
}
const datasourcePromise = this.datasourceSrv.get(annotation.datasource);
dsPromises.push(datasourcePromise);
promises.push(
this.datasourceSrv
.get(annotation.datasource)
datasourcePromise
.then(datasource => {
// issue query against data source
return datasource.annotationQuery({
@@ -122,7 +125,7 @@ export class AnnotationsSrv {
})
);
}
this.datasourcePromises = this.$q.all(dsPromises);
this.globalAnnotationsPromise = this.$q.all(promises);
return this.globalAnnotationsPromise;
}
+17 -1
View File
@@ -1,7 +1,8 @@
import { StoreState } from 'app/types';
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from 'app/core/services/backend_srv';
import appEvents from 'app/core/app_events';
import { loadPluginDashboards } from '../../plugins/state/actions';
import {
DashboardAcl,
DashboardAclDTO,
@@ -113,3 +114,18 @@ export function addDashboardPermission(dashboardId: number, newItem: NewDashboar
await dispatch(getDashboardPermissions(dashboardId));
};
}
export function importDashboard(data, dashboardTitle: string): ThunkResult<void> {
return async dispatch => {
await getBackendSrv().post('/api/dashboards/import', data);
appEvents.emit('alert-success', ['Dashboard Imported', dashboardTitle]);
dispatch(loadPluginDashboards());
};
}
export function removeDashboard(uri: string): ThunkResult<void> {
return async dispatch => {
await getBackendSrv().delete(`/api/dashboards/${uri}`);
dispatch(loadPluginDashboards());
};
}
@@ -7,13 +7,13 @@ export class SubmenuCtrl {
dashboard: any;
/** @ngInject */
constructor(private $rootScope, private variableSrv, private $location) {
constructor(private variableSrv, private $location) {
this.annotations = this.dashboard.templating.list;
this.variables = this.variableSrv.variables;
}
annotationStateChanged() {
this.$rootScope.$broadcast('refresh');
this.dashboard.startRefresh();
}
variableUpdated(variable) {
@@ -0,0 +1,63 @@
import React from 'react';
import { shallow } from 'enzyme';
import DashboardsTable, { Props } from './DashboardsTable';
import { PluginDashboard } from '../../types';
const setup = (propOverrides?: object) => {
const props: Props = {
dashboards: [] as PluginDashboard[],
onImport: jest.fn(),
onRemove: jest.fn(),
};
Object.assign(props, propOverrides);
return shallow(<DashboardsTable {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render table', () => {
const wrapper = setup({
dashboards: [
{
dashboardId: 0,
description: '',
folderId: 0,
imported: false,
importedRevision: 0,
importedUri: '',
importedUrl: '',
path: 'dashboards/carbon_metrics.json',
pluginId: 'graphite',
removed: false,
revision: 1,
slug: '',
title: 'Graphite Carbon Metrics',
},
{
dashboardId: 0,
description: '',
folderId: 0,
imported: true,
importedRevision: 0,
importedUri: '',
importedUrl: '',
path: 'dashboards/carbon_metrics.json',
pluginId: 'graphite',
removed: false,
revision: 1,
slug: '',
title: 'Graphite Carbon Metrics',
},
],
});
expect(wrapper).toMatchSnapshot();
});
});
@@ -0,0 +1,55 @@
import React, { SFC } from 'react';
import { PluginDashboard } from '../../types';
export interface Props {
dashboards: PluginDashboard[];
onImport: (dashboard, overwrite) => void;
onRemove: (dashboard) => void;
}
const DashboardsTable: SFC<Props> = ({ dashboards, onImport, onRemove }) => {
function buttonText(dashboard: PluginDashboard) {
return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
}
return (
<table className="filter-table">
<tbody>
{dashboards.map((dashboard, index) => {
return (
<tr key={`${dashboard.dashboardId}-${index}`}>
<td className="width-1">
<i className="icon-gf icon-gf-dashboard" />
</td>
<td>
{dashboard.imported ? (
<a href={dashboard.importedUrl}>{dashboard.title}</a>
) : (
<span>{dashboard.title}</span>
)}
</td>
<td style={{ textAlign: 'right' }}>
{!dashboard.imported ? (
<button className="btn btn-secondary btn-small" onClick={() => onImport(dashboard, false)}>
Import
</button>
) : (
<button className="btn btn-secondary btn-small" onClick={() => onImport(dashboard, true)}>
{buttonText(dashboard)}
</button>
)}
{dashboard.imported && (
<button className="btn btn-danger btn-small" onClick={() => onRemove(dashboard)}>
<i className="fa fa-trash" />
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
};
export default DashboardsTable;
@@ -0,0 +1,29 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DataSourceDashboards, Props } from './DataSourceDashboards';
import { DataSource, NavModel, PluginDashboard } from 'app/types';
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
dashboards: [] as PluginDashboard[],
dataSource: {} as DataSource,
pageId: 1,
importDashboard: jest.fn(),
loadDataSource: jest.fn(),
loadPluginDashboards: jest.fn(),
removeDashboard: jest.fn(),
};
Object.assign(props, propOverrides);
return shallow(<DataSourceDashboards {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});
@@ -0,0 +1,93 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import DashboardTable from './DashboardsTable';
import { DataSource, NavModel, PluginDashboard } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { getRouteParamsId } from 'app/core/selectors/location';
import { loadDataSource } from './state/actions';
import { loadPluginDashboards } from '../plugins/state/actions';
import { importDashboard, removeDashboard } from '../dashboard/state/actions';
import { getDataSource } from './state/selectors';
export interface Props {
navModel: NavModel;
dashboards: PluginDashboard[];
dataSource: DataSource;
pageId: number;
importDashboard: typeof importDashboard;
loadDataSource: typeof loadDataSource;
loadPluginDashboards: typeof loadPluginDashboards;
removeDashboard: typeof removeDashboard;
}
export class DataSourceDashboards extends PureComponent<Props> {
async componentDidMount() {
const { loadDataSource, pageId } = this.props;
await loadDataSource(pageId);
this.props.loadPluginDashboards();
}
onImport = (dashboard: PluginDashboard, overwrite: boolean) => {
const { dataSource, importDashboard } = this.props;
const data = {
pluginId: dashboard.pluginId,
path: dashboard.path,
overwrite: overwrite,
inputs: [],
};
if (dataSource) {
data.inputs.push({
name: '*',
type: 'datasource',
pluginId: dataSource.type,
value: dataSource.name,
});
}
importDashboard(data, dashboard.title);
};
onRemove = (dashboard: PluginDashboard) => {
this.props.removeDashboard(dashboard.importedUri);
};
render() {
const { dashboards, navModel } = this.props;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<DashboardTable
dashboards={dashboards}
onImport={(dashboard, overwrite) => this.onImport(dashboard, overwrite)}
onRemove={dashboard => this.onRemove(dashboard)}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
const pageId = getRouteParamsId(state.location);
return {
navModel: getNavModel(state.navIndex, `datasource-dashboards-${pageId}`),
pageId: pageId,
dashboards: state.plugins.dashboards,
dataSource: getDataSource(state.dataSources, pageId),
};
}
const mapDispatchToProps = {
importDashboard,
loadDataSource,
loadPluginDashboards,
removeDashboard,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceDashboards));
@@ -0,0 +1,125 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { DataSource, Plugin } from 'app/types';
export interface Props {
dataSource: DataSource;
dataSourceMeta: Plugin;
}
interface State {
name: string;
}
enum DataSourceStates {
Alpha = 'alpha',
Beta = 'beta',
}
export class DataSourceSettings extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
name: props.dataSource.name,
};
}
onNameChange = event => {
this.setState({
name: event.target.value,
});
};
onSubmit = event => {
event.preventDefault();
console.log(event);
};
onDelete = event => {
console.log(event);
};
isReadyOnly() {
return this.props.dataSource.readOnly === true;
}
shouldRenderInfoBox() {
const { state } = this.props.dataSourceMeta;
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
}
getInfoText() {
const { dataSourceMeta } = this.props;
switch (dataSourceMeta.state) {
case DataSourceStates.Alpha:
return (
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
' will include breaking changes.'
);
case DataSourceStates.Beta:
return (
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
' development and could be missing important features.'
);
}
return null;
}
render() {
const { name } = this.state;
return (
<div>
<h3 className="page-sub-heading">Settings</h3>
<form onSubmit={this.onSubmit}>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<span className="gf-form-label width-10">Name</span>
<input
className="gf-form-input max-width-23"
type="text"
value={name}
placeholder="name"
onChange={this.onNameChange}
required
/>
</div>
</div>
</div>
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
{this.isReadyOnly() && (
<div className="grafana-info-box span8">
This datasource was added by config and cannot be modified using the UI. Please contact your server admin
to update this datasource.
</div>
)}
<div className="gf-form-button-row">
<button type="submit" className="btn btn-success" disabled={this.isReadyOnly()} onClick={this.onSubmit}>
Save &amp; Test
</button>
<button type="submit" className="btn btn-danger" disabled={this.isReadyOnly()} onClick={this.onDelete}>
Delete
</button>
<a className="btn btn-inverse" href="datasources">
Back
</a>
</div>
</form>
</div>
);
}
}
function mapStateToProps(state) {
return {
dataSource: state.dataSources.dataSource,
dataSourceMeta: state.dataSources.dataSourceMeta,
};
}
export default connect(mapStateToProps)(DataSourceSettings);
@@ -4,7 +4,6 @@ import { hot } from 'react-hot-loader';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import { NavModel, Plugin } from 'app/types';
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
import { updateLocation } from '../../core/actions';
import { getNavModel } from 'app/core/selectors/navModel';
import { getDataSourceTypes } from './state/selectors';
@@ -13,7 +12,6 @@ export interface Props {
dataSourceTypes: Plugin[];
addDataSource: typeof addDataSource;
loadDataSourceTypes: typeof loadDataSourceTypes;
updateLocation: typeof updateLocation;
dataSourceTypeSearchQuery: string;
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
}
@@ -81,7 +79,6 @@ function mapStateToProps(state) {
const mapDispatchToProps = {
addDataSource,
loadDataSourceTypes,
updateLocation,
setDataSourceTypeSearchQuery,
};
@@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<table
className="filter-table"
>
<tbody />
</table>
`;
exports[`Render should render table 1`] = `
<table
className="filter-table"
>
<tbody>
<tr
key="0-0"
>
<td
className="width-1"
>
<i
className="icon-gf icon-gf-dashboard"
/>
</td>
<td>
<span>
Graphite Carbon Metrics
</span>
</td>
<td
style={
Object {
"textAlign": "right",
}
}
>
<button
className="btn btn-secondary btn-small"
onClick={[Function]}
>
Import
</button>
</td>
</tr>
<tr
key="0-1"
>
<td
className="width-1"
>
<i
className="icon-gf icon-gf-dashboard"
/>
</td>
<td>
<a
href=""
>
Graphite Carbon Metrics
</a>
</td>
<td
style={
Object {
"textAlign": "right",
}
}
>
<button
className="btn btn-secondary btn-small"
onClick={[Function]}
>
Update
</button>
<button
className="btn btn-danger btn-small"
onClick={[Function]}
>
<i
className="fa fa-trash"
/>
</button>
</td>
</tr>
</tbody>
</table>
`;
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
>
<DashboardsTable
dashboards={Array []}
onImport={[Function]}
onRemove={[Function]}
/>
</div>
</div>
`;
@@ -2,12 +2,15 @@ import { ThunkAction } from 'redux-thunk';
import { DataSource, Plugin, StoreState } from 'app/types';
import { getBackendSrv } from '../../../core/services/backend_srv';
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
import { updateLocation } from '../../../core/actions';
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions';
import { UpdateLocationAction } from '../../../core/actions/location';
import { buildNavModel } from './navModel';
export enum ActionTypes {
LoadDataSources = 'LOAD_DATA_SOURCES',
LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
LoadDataSource = 'LOAD_DATA_SOURCE',
LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META',
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
@@ -38,11 +41,31 @@ export interface SetDataSourceTypeSearchQueryAction {
payload: string;
}
export interface LoadDataSourceAction {
type: ActionTypes.LoadDataSource;
payload: DataSource;
}
export interface LoadDataSourceMetaAction {
type: ActionTypes.LoadDataSourceMeta;
payload: Plugin;
}
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
type: ActionTypes.LoadDataSources,
payload: dataSources,
});
const dataSourceLoaded = (dataSource: DataSource): LoadDataSourceAction => ({
type: ActionTypes.LoadDataSource,
payload: dataSource,
});
const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction => ({
type: ActionTypes.LoadDataSourceMeta,
payload: dataSourceMeta,
});
const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
type: ActionTypes.LoadDataSourceTypes,
payload: dataSourceTypes,
@@ -69,7 +92,10 @@ export type Action =
| SetDataSourcesLayoutModeAction
| UpdateLocationAction
| LoadDataSourceTypesAction
| SetDataSourceTypeSearchQueryAction;
| SetDataSourceTypeSearchQueryAction
| LoadDataSourceAction
| UpdateNavIndexAction
| LoadDataSourceMetaAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
@@ -80,6 +106,16 @@ export function loadDataSources(): ThunkResult<void> {
};
}
export function loadDataSource(id: number): ThunkResult<void> {
return async dispatch => {
const dataSource = await getBackendSrv().get(`/api/datasources/${id}`);
const pluginInfo = await getBackendSrv().get(`/api/plugins/${dataSource.type}/settings`);
dispatch(dataSourceLoaded(dataSource));
dispatch(dataSourceMetaLoaded(pluginInfo));
dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo)));
};
}
export function addDataSource(plugin: Plugin): ThunkResult<void> {
return async (dispatch, getStore) => {
await dispatch(loadDataSources());
@@ -0,0 +1,109 @@
import { DataSource, NavModel, NavModelItem, PluginMeta } from 'app/types';
import config from 'app/core/config';
export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): NavModelItem {
const navModel = {
img: pluginMeta.info.logos.large,
id: 'datasource-' + dataSource.id,
subTitle: `Type: ${pluginMeta.name}`,
url: '',
text: dataSource.name,
breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }],
children: [
{
active: false,
icon: 'fa fa-fw fa-sliders',
id: `datasource-settings-${dataSource.id}`,
text: 'Settings',
url: `datasources/edit/${dataSource.id}`,
},
],
};
if (pluginMeta.includes && hasDashboards(pluginMeta.includes)) {
navModel.children.push({
active: false,
icon: 'fa fa-fw fa-th-large',
id: `datasource-dashboards-${dataSource.id}`,
text: 'Dashboards',
url: `datasources/edit/${dataSource.id}/dashboards`,
});
}
if (config.buildInfo.isEnterprise) {
navModel.children.push({
active: false,
icon: 'fa fa-fw fa-lock',
id: `datasource-permissions-${dataSource.id}`,
text: 'Permissions',
url: `datasources/edit/${dataSource.id}/permissions`,
});
}
return navModel;
}
export function getDataSourceLoadingNav(pageName: string): NavModel {
const main = buildNavModel(
{
access: '',
basicAuth: false,
database: '',
id: 1,
isDefault: false,
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
name: 'Loading',
orgId: 1,
password: '',
readOnly: false,
type: 'Loading',
typeLogoUrl: 'public/img/icn-datasource.svg',
url: '',
user: '',
},
{
id: '1',
name: '',
info: {
author: {
name: '',
url: '',
},
description: '',
links: [''],
logos: {
large: '',
small: '',
},
screenshots: '',
updated: '',
version: '',
},
includes: [{ type: '', name: '', path: '' }],
}
);
let node: NavModelItem;
// find active page
for (const child of main.children) {
if (child.id.indexOf(pageName) > 0) {
child.active = true;
node = child;
break;
}
}
return {
main: main,
node: node,
};
}
function hasDashboards(includes) {
return (
includes.filter(include => {
return include.type === 'dashboard';
}).length > 0
);
}
@@ -4,11 +4,13 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec
const initialState: DataSourcesState = {
dataSources: [] as DataSource[],
dataSource: {} as DataSource,
layoutMode: LayoutModes.Grid,
searchQuery: '',
dataSourcesCount: 0,
dataSourceTypes: [] as Plugin[],
dataSourceTypeSearchQuery: '',
dataSourceMeta: {} as Plugin,
hasFetched: false,
};
@@ -17,6 +19,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
case ActionTypes.LoadDataSources:
return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
case ActionTypes.LoadDataSource:
return { ...state, dataSource: action.payload };
case ActionTypes.SetDataSourcesSearchQuery:
return { ...state, searchQuery: action.payload };
@@ -28,6 +33,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
case ActionTypes.SetDataSourceTypeSearchQuery:
return { ...state, dataSourceTypeSearchQuery: action.payload };
case ActionTypes.LoadDataSourceMeta:
return { ...state, dataSourceMeta: action.payload };
}
return state;
@@ -1,3 +1,5 @@
import { DataSource } from '../../../types';
export const getDataSources = state => {
const regex = new RegExp(state.searchQuery, 'i');
@@ -14,6 +16,13 @@ export const getDataSourceTypes = state => {
});
};
export const getDataSource = (state, dataSourceId): DataSource | null => {
if (state.dataSource.id === parseInt(dataSourceId, 10)) {
return state.dataSource;
}
return null;
};
export const getDataSourcesSearchQuery = state => state.searchQuery;
export const getDataSourcesLayoutMode = state => state.layoutMode;
export const getDataSourcesCount = state => state.dataSourcesCount;
+3 -1
View File
@@ -644,7 +644,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
/>
)}
{supportsTable && showingTable ? (
<Table className="m-t-3" data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
<div className="panel-container">
<Table data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
</div>
) : null}
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={loading} /> : null}
</main>
@@ -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];
+7 -1
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);
}
+34 -63
View File
@@ -1,84 +1,55 @@
import _ from 'lodash';
import React, { PureComponent } from 'react';
import ReactTable from 'react-table';
import TableModel from 'app/core/table_model';
const EMPTY_TABLE = new TableModel();
interface TableProps {
className?: string;
data: TableModel;
loading: boolean;
onClickCell?: (columnKey: string, rowValue: string) => void;
}
interface SFCCellProps {
columnIndex: number;
onClickCell?: (columnKey: string, rowValue: string, columnIndex: number, rowIndex: number, table: TableModel) => void;
rowIndex: number;
table: TableModel;
value: string;
function prepareRows(rows, columnNames) {
return rows.map(cells => _.zipObject(columnNames, cells));
}
function Cell(props: SFCCellProps) {
const { columnIndex, rowIndex, table, value, onClickCell } = props;
const column = table.columns[columnIndex];
if (column && column.filterable && onClickCell) {
const onClick = event => {
event.preventDefault();
onClickCell(column.text, value, columnIndex, rowIndex, table);
export default class Table extends PureComponent<TableProps> {
getCellProps = (state, rowInfo, column) => {
return {
onClick: () => {
const columnKey = column.Header;
const rowValue = rowInfo.row[columnKey];
this.props.onClickCell(columnKey, rowValue);
},
};
return (
<td>
<a className="link" onClick={onClick}>
{value}
</a>
</td>
);
}
return <td>{value}</td>;
}
};
export default class Table extends PureComponent<TableProps, {}> {
render() {
const { className = '', data, loading, onClickCell } = this.props;
const { data, loading } = this.props;
const tableModel = data || EMPTY_TABLE;
if (!loading && data && data.rows.length === 0) {
return (
<table className={`${className} filter-table`}>
<thead>
<tr>
<th>Table</th>
</tr>
</thead>
<tbody>
<tr>
<td className="muted">The queries returned no data for a table.</td>
</tr>
</tbody>
</table>
);
}
const columnNames = tableModel.columns.map(({ text }) => text);
const columns = tableModel.columns.map(({ filterable, text }) => ({
Header: text,
accessor: text,
show: text !== 'Time',
Cell: row => <span className={filterable ? 'link' : ''}>{row.value}</span>,
}));
const noDataText = data ? 'The queries returned no data for a table.' : '';
return (
<table className={`${className} filter-table`}>
<thead>
<tr>{tableModel.columns.map(col => <th key={col.text}>{col.text}</th>)}</tr>
</thead>
<tbody>
{tableModel.rows.map((row, i) => (
<tr key={i}>
{row.map((value, j) => (
<Cell
key={j}
columnIndex={j}
rowIndex={i}
value={String(value)}
table={data}
onClickCell={onClickCell}
/>
))}
</tr>
))}
</tbody>
</table>
<ReactTable
columns={columns}
data={tableModel.rows}
getTdProps={this.getCellProps}
loading={loading}
minRows={0}
noDataText={noDataText}
resolveData={data => prepareRows(data, columnNames)}
showPagination={data}
/>
);
}
}
@@ -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
View File
@@ -2,6 +2,5 @@ import './plugin_edit_ctrl';
import './plugin_page_ctrl';
import './import_list/import_list';
import './ds_edit_ctrl';
import './ds_dashboards_ctrl';
import './datasource_srv';
import './plugin_component';
@@ -1,7 +0,0 @@
<page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body" ng-if="ctrl.datasourceMeta">
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
</div>
+22 -1
View File
@@ -2,9 +2,11 @@ import { Plugin, StoreState } from 'app/types';
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from '../../../core/services/backend_srv';
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
import { PluginDashboard } from '../../../types/plugins';
export enum ActionTypes {
LoadPlugins = 'LOAD_PLUGINS',
LoadPluginDashboards = 'LOAD_PLUGIN_DASHBOARDS',
SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY',
SetLayoutMode = 'SET_LAYOUT_MODE',
}
@@ -14,6 +16,11 @@ export interface LoadPluginsAction {
payload: Plugin[];
}
export interface LoadPluginDashboardsAction {
type: ActionTypes.LoadPluginDashboards;
payload: PluginDashboard[];
}
export interface SetPluginsSearchQueryAction {
type: ActionTypes.SetPluginsSearchQuery;
payload: string;
@@ -39,7 +46,12 @@ const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({
payload: plugins,
});
export type Action = LoadPluginsAction | SetPluginsSearchQueryAction | SetLayoutModeAction;
const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadPluginDashboardsAction => ({
type: ActionTypes.LoadPluginDashboards,
payload: dashboards,
});
export type Action = LoadPluginsAction | LoadPluginDashboardsAction | SetPluginsSearchQueryAction | SetLayoutModeAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
@@ -49,3 +61,12 @@ export function loadPlugins(): ThunkResult<void> {
dispatch(pluginsLoaded(result));
};
}
export function loadPluginDashboards(): ThunkResult<void> {
return async (dispatch, getStore) => {
const dataSourceType = getStore().dataSources.dataSource.type;
const response = await getBackendSrv().get(`api/plugins/${dataSourceType}/dashboards`);
dispatch(pluginDashboardsLoaded(response));
};
}
@@ -1,5 +1,6 @@
import _ from 'lodash';
import { DataSource, PluginMeta, NavModel } from 'app/types';
import config from 'app/core/config';
export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel {
let title = 'New';
@@ -38,6 +39,16 @@ export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: s
});
}
if (config.buildInfo.isEnterprise) {
main.children.push({
active: currentPage === 'datasource-permissions',
icon: 'fa fa-fw fa-lock',
id: 'datasource-permissions',
text: 'Permissions',
url: `datasources/edit/${ds.id}/permissions`,
});
}
return {
main: main,
node: _.find(main.children, { active: true }),
@@ -1,12 +1,14 @@
import { Action, ActionTypes } from './actions';
import { Plugin, PluginsState } from 'app/types';
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
import { PluginDashboard } from '../../../types/plugins';
export const initialState: PluginsState = {
plugins: [] as Plugin[],
searchQuery: '',
layoutMode: LayoutModes.Grid,
hasFetched: false,
dashboards: [] as PluginDashboard[],
};
export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
@@ -19,6 +21,9 @@ export const pluginsReducer = (state = initialState, action: Action): PluginsSta
case ActionTypes.SetLayoutMode:
return { ...state, layoutMode: action.payload };
case ActionTypes.LoadPluginDashboards:
return { ...state, dashboards: action.payload };
}
return state;
};
@@ -1,5 +1,8 @@
// Libaries
import angular from 'angular';
import _ from 'lodash';
// Utils & Services
import coreModule from 'app/core/core_module';
import { variableTypes } from './variable';
import { Graph } from 'app/core/utils/dag';
@@ -291,9 +294,11 @@ export class VariableSrv {
createGraph() {
const g = new Graph();
this.variables.forEach(v1 => {
g.createNode(v1.name);
this.variables.forEach(v => {
g.createNode(v.name);
});
this.variables.forEach(v1 => {
this.variables.forEach(v2 => {
if (v1 === v2) {
return;
@@ -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>
@@ -0,0 +1,10 @@
export class MssqlConfigCtrl {
static templateUrl = 'partials/config.html';
current: any;
/** @ngInject */
constructor($scope) {
this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
}
}
@@ -1,9 +1,6 @@
import { MssqlDatasource } from './datasource';
import { MssqlQueryCtrl } from './query_ctrl';
class MssqlConfigCtrl {
static templateUrl = 'partials/config.html';
}
import { MssqlConfigCtrl } from './config_ctrl';
const defaultQuery = `SELECT
<time_column> as time,
@@ -27,6 +27,22 @@
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">Encrypt</label>
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.encrypt" ng-options="mode for mode in ['disable', 'false', 'true']" ng-init="ctrl.current.jsonData.encrypt"></select>
<info-popover mode="right-absolute">
Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server.
<ul>
<li><i>disable</i> - Data sent between client and server is not encrypted.</li>
<li><i>false</i> - Data sent between client and server is not encrypted beyond the login packet. (default)</li>
<li><i>true</i> - Data sent between client and server is encrypted.</li>
</ul>
If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect.
</info-popover>
</div>
</div>
</div>
<b>Connection limits</b>
@@ -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 => {
+10 -1
View File
@@ -156,7 +156,16 @@ class GraphCtrl extends MetricsPanelCtrl {
panel: this.panel,
range: this.range,
});
return super.issueQueries(datasource);
/* Wait for annotationSrv requests to get datasources to
* resolve before issuing queries. This allows the annotations
* service to fire annotations queries before graph queries
* (but not wait for completion). This resolves
* issue 11806.
*/
return this.annotationsSrv.datasourcePromises.then(r => {
return super.issueQueries(datasource);
});
}
zoomOut(evt) {
+5 -3
View File
@@ -13,6 +13,7 @@ import FolderPermissions from 'app/features/folders/FolderPermissions';
import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
import UsersListPage from 'app/features/users/UsersListPage';
import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
/** @ngInject */
export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -78,9 +79,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl',
})
.when('/datasources/edit/:id/dashboards', {
templateUrl: 'public/app/features/plugins/partials/ds_dashboards.html',
controller: 'DataSourceDashboardsCtrl',
controllerAs: 'ctrl',
template: '<react-container />',
resolve: {
component: () => DataSourceDashboards,
},
})
.when('/datasources/new', {
template: '<react-container />',
+8 -2
View File
@@ -11,7 +11,7 @@ import pluginReducers from 'app/features/plugins/state/reducers';
import dataSourcesReducers from 'app/features/datasources/state/reducers';
import usersReducers from 'app/features/users/state/reducers';
const rootReducer = combineReducers({
const rootReducers = {
...sharedReducers,
...alertingReducers,
...teamsReducers,
@@ -21,13 +21,19 @@ const rootReducer = combineReducers({
...pluginReducers,
...dataSourcesReducers,
...usersReducers,
});
};
export let store;
export function addRootReducer(reducers) {
Object.assign(rootReducers, ...reducers);
}
export function configureStore() {
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers(rootReducers);
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));
+9
View File
@@ -61,6 +61,11 @@ export enum PermissionLevel {
Admin = 4,
}
export enum DataSourcePermissionLevel {
Query = 1,
Admin = 2,
}
export enum AclTarget {
Team = 'Team',
User = 'User',
@@ -73,6 +78,10 @@ export interface AclTargetInfo {
text: string;
}
export const dataSourceAclLevels = [
{ value: DataSourcePermissionLevel.Query, label: 'Query', description: 'Can query data source.' },
];
export const dashboardAclTargets: AclTargetInfo[] = [
{ value: AclTarget.Team, text: 'Team' },
{ value: AclTarget.User, text: 'User' },
+5 -3
View File
@@ -12,10 +12,10 @@ export interface DataSource {
password: string;
user: string;
database: string;
basicAuth: false;
isDefault: false;
basicAuth: boolean;
isDefault: boolean;
jsonData: { authType: string; defaultRegion: string };
readOnly: false;
readOnly: boolean;
}
export interface DataSourcesState {
@@ -25,5 +25,7 @@ export interface DataSourcesState {
layoutMode: LayoutMode;
dataSourcesCount: number;
dataSourceTypes: Plugin[];
dataSource: DataSource;
dataSourceMeta: Plugin;
hasFetched: boolean;
}
+2 -1
View File
@@ -8,7 +8,6 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
import { Invitee, OrgUser, User, UsersState } from './user';
import { DataSource, DataSourcesState } from './datasources';
import { PluginMeta, Plugin, PluginsState } from './plugins';
import {
TimeRange,
LoadingState,
@@ -22,6 +21,7 @@ import {
DataQueryOptions,
} from './series';
import { PanelProps } from './panel';
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
export {
Team,
@@ -69,6 +69,7 @@ export {
DataQuery,
DataQueryResponse,
DataQueryOptions,
PluginDashboard,
};
export interface StoreState {
+17
View File
@@ -62,9 +62,26 @@ export interface Plugin {
type: string;
}
export interface PluginDashboard {
dashboardId: number;
description: string;
folderId: number;
imported: boolean;
importedRevision: number;
importedUri: string;
importedUrl: string;
path: string;
pluginId: string;
removed: boolean;
revision: number;
slug: string;
title: string;
}
export interface PluginsState {
plugins: Plugin[];
searchQuery: string;
layoutMode: string;
hasFetched: boolean;
dashboards: PluginDashboard[];
}
+4 -1
View File
@@ -1,4 +1,7 @@
// vendor
// DEPENDENCIES
@import '../../node_modules/react-table/react-table.css';
// VENDOR
@import '../vendor/css/timepicker.css';
@import '../vendor/css/spectrum.css';
@import '../vendor/css/rc-cascader.scss';
@@ -2,6 +2,7 @@
font-size: $font-size-root;
font-family: $font-family-monospace;
height: auto;
word-break: break-word;
}
.slate-query-field-wrapper {
+58 -1
View File
@@ -126,7 +126,7 @@
}
.query-row-tools {
width: 6rem;
white-space: nowrap;
}
.query-row-field {
@@ -186,3 +186,60 @@
margin: 0.25em 0.5em 0.5em;
}
}
// ReactTable basic overrides (does not include pivot/groups/filters)
// When integrating ReactTable as new panel plugin, move to _panel_table.scss
.ReactTable {
border: none;
// Allow some space for the no-data text
min-height: 120px;
}
.ReactTable .rt-thead.-header {
box-shadow: none;
background: $list-item-bg;
border-top: 2px solid $body-bg;
border-bottom: 2px solid $body-bg;
height: 2em;
}
.ReactTable .rt-thead.-header .rt-th {
text-align: left;
color: $blue;
font-weight: 500;
}
.ReactTable .rt-thead .rt-td,
.ReactTable .rt-thead .rt-th {
padding: 0.45em 0 0.45em 1.1em;
border-right: none;
box-shadow: none;
}
.ReactTable .rt-tbody .rt-td {
padding: 0.45em 0 0.45em 1.1em;
border-bottom: 2px solid $body-bg;
border-right: 2px solid $body-bg;
}
.ReactTable .rt-tbody .rt-td:last-child {
border-right: none;
}
.ReactTable .-pagination .-btn {
color: $blue;
background: $list-item-bg;
}
.ReactTable .-pagination input,
.ReactTable .-pagination select {
color: $input-color;
background-color: $input-bg;
}
.ReactTable .-loading {
background: $input-bg;
}
.ReactTable .-loading.-active {
opacity: 0.8;
}
.ReactTable .-loading > div {
color: $input-color;
}
.ReactTable .rt-tr .rt-td:last-child {
text-align: right;
}
+10 -7
View File
@@ -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
};
+6 -1935
View File
File diff suppressed because it is too large Load Diff