diff --git a/.gitignore b/.gitignore
index d957993f487..f743dcdc015 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,8 @@ conf/custom.ini
fig.yml
docker-compose.yml
docker-compose.yaml
+/conf/dashboards/custom.yaml
+/conf/datasources/custom.yaml
profile.cov
/grafana
.notouch
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a75ad758c8..1b4adfcdda2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@
* **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda)
* **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871)
* **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798)
-
+* **Systemd**: Use systemd notification ready flag [#10024](https://github.com/grafana/grafana/issues/10024), thx [@jgrassler](https://github.com/jgrassler)
## Tech
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
diff --git a/LICENSE.md b/LICENSE.md
index 4c6a79691f0..d6456956733 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,14 +1,202 @@
-Copyright 2014-2017 Torkel Ödegaard, Raintank Inc.
-Licensed under the Apache License, Version 2.0 (the "License"); you
-may not use this file except in compliance with the License. You may
-obtain a copy of the License at
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
- http://www.apache.org/licenses/LICENSE-2.0
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied. See the License for the specific language governing
-permissions and limitations under the License.
+ 1. Definitions.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/NOTICE.md b/NOTICE.md
index 171332f00d4..ca148971b62 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1,16 +1,6 @@
-This software is based on Kibana:
-========================================
+Copyright 2014-2017 Grafana Labs
+
+This software is based on Kibana:
Copyright 2012-2013 Elasticsearch BV
-Licensed under the Apache License, Version 2.0 (the "License"); you
-may not use this file except in compliance with the License. You may
-obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied. See the License for the specific language governing
-permissions and limitations under the License.
diff --git a/build.go b/build.go
index a1d1d3012ab..1c61c72d5dc 100644
--- a/build.go
+++ b/build.go
@@ -95,9 +95,9 @@ func main() {
case "package":
grunt(gruntBuildArg("release")...)
- if runtime.GOOS != "windows" {
- createLinuxPackages()
- }
+ if runtime.GOOS != "windows" {
+ createLinuxPackages()
+ }
case "pkg-rpm":
grunt(gruntBuildArg("release")...)
diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/docker/blocks/mysql_tests/docker-compose.yaml
index 646cc7ee369..c6c3097d463 100644
--- a/docker/blocks/mysql_tests/docker-compose.yaml
+++ b/docker/blocks/mysql_tests/docker-compose.yaml
@@ -7,3 +7,4 @@
MYSQL_PASSWORD: password
ports:
- "3306:3306"
+ tmpfs: /var/lib/mysql:rw
diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/docker/blocks/postgres_tests/docker-compose.yaml
index 3d9a82c034c..44b66e8e558 100644
--- a/docker/blocks/postgres_tests/docker-compose.yaml
+++ b/docker/blocks/postgres_tests/docker-compose.yaml
@@ -5,3 +5,4 @@
POSTGRES_PASSWORD: grafanatest
ports:
- "5432:5432"
+ tmpfs: /var/lib/postgresql/data:rw
\ No newline at end of file
diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md
index 70d9d7a81f3..0745794496f 100644
--- a/docs/sources/administration/provisioning.md
+++ b/docs/sources/administration/provisioning.md
@@ -65,13 +65,14 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe
Tool | Project
-----|------------
Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
+Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
## Datasources
-> This feature is available from v4.7
+> This feature is available from v5.0
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index 69c6f667062..d9f048e0371 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -127,6 +127,12 @@ A query can returns multiple columns and Grafana will automatically create a lis
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
```
+To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
+
+```sql
+SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
+```
+
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
```sql
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 2b111d51f0e..e9d65b8f327 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -139,6 +139,12 @@ A query can return multiple columns and Grafana will automatically create a list
SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city
```
+To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
+
+```sql
+SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
+```
+
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
```sql
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 483774f94f5..d454455a26b 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -93,7 +93,10 @@ Directory where grafana will automatically scan and look for plugins
### datasources
-Config files containing datasources that will be configured at startup
+> This feature is available in 5.0+
+
+Config files containing datasources that will be configured at startup.
+You can read more about the config files at the [provisioning page](/administration/provisioning/#datasources).
## [server]
diff --git a/package.json b/package.json
index 492bbf5359e..59608e80403 100644
--- a/package.json
+++ b/package.json
@@ -125,7 +125,6 @@
"lodash": "^4.17.4",
"moment": "^2.18.1",
"mousetrap": "^1.6.0",
- "ngreact": "^0.4.1",
"perfect-scrollbar": "^1.2.0",
"prop-types": "^15.6.0",
"react": "^16.1.1",
diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service
index 3e018e8b176..b23e5196e17 100644
--- a/packaging/rpm/systemd/grafana-server.service
+++ b/packaging/rpm/systemd/grafana-server.service
@@ -9,7 +9,7 @@ After=postgresql.service mariadb.service mysql.service
EnvironmentFile=/etc/sysconfig/grafana-server
User=grafana
Group=grafana
-Type=simple
+Type=notify
Restart=on-failure
WorkingDirectory=/usr/share/grafana
RuntimeDirectory=grafana
diff --git a/pkg/api/index.go b/pkg/api/index.go
index eb9e70bb404..058f5541eb5 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Create",
+ Id: "create",
Icon: "fa fa-fw fa-plus",
Url: "#",
Children: []*dtos.NavLink{
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
- {Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
- {Text: "Import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"},
+ {Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"},
+ {Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
},
})
}
@@ -103,7 +104,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true},
{Divider: true, HideFromTabs: true},
- {Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"},
+ {Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"},
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"},
}
@@ -117,6 +118,21 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs,
})
+ dashboardFolderChildNavs := []*dtos.NavLink{
+ {Text: "Dashboards", Id: "manage-folder-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-th-large"},
+ {Text: "Permissions", Id: "manage-folder-permissions", Url: setting.AppSubUrl + "/dashboards?1", Icon: "fa fa-fw fa-lock"},
+ }
+
+ data.NavTree = append(data.NavTree, &dtos.NavLink{
+ Text: "Dashboards",
+ Id: "manage-folder",
+ SubTitle: "Manage folder dashboards & permissions",
+ Icon: "fa fa-folder-open",
+ Url: setting.AppSubUrl + "/",
+ HideFromMenu: true,
+ Children: dashboardFolderChildNavs,
+ })
+
if c.IsSignedIn {
profileNode := &dtos.NavLink{
Text: c.SignedInUser.Name,
diff --git a/pkg/api/render.go b/pkg/api/render.go
index 2a5ce27c210..6d1b49e4462 100644
--- a/pkg/api/render.go
+++ b/pkg/api/render.go
@@ -10,7 +10,11 @@ import (
)
func RenderToPng(c *middleware.Context) {
- queryReader := util.NewUrlQueryReader(c.Req.URL)
+ queryReader, err := util.NewUrlQueryReader(c.Req.URL)
+ if err != nil {
+ c.Handle(400, "Render parameters error", err)
+ return
+ }
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
renderOpts := &renderer.RenderOpts{
diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go
index 1d3ac092734..f5c6b0d1cee 100644
--- a/pkg/cmd/grafana-server/server.go
+++ b/pkg/cmd/grafana-server/server.go
@@ -3,7 +3,9 @@ package main
import (
"context"
"flag"
+ "fmt"
"io/ioutil"
+ "net"
"os"
"path/filepath"
"strconv"
@@ -96,6 +98,7 @@ func (g *GrafanaServerImpl) Start() {
return
}
+ SendSystemdNotification("READY=1")
g.startHttpServer()
}
@@ -169,3 +172,28 @@ func (g *GrafanaServerImpl) writePIDFile() {
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
}
+
+func SendSystemdNotification(state string) error {
+ notifySocket := os.Getenv("NOTIFY_SOCKET")
+
+ if notifySocket == "" {
+ return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.")
+ }
+
+ socketAddr := &net.UnixAddr{
+ Name: notifySocket,
+ Net: "unixgram",
+ }
+
+ conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
+
+ if err != nil {
+ return err
+ }
+
+ _, err = conn.Write([]byte(state))
+
+ conn.Close()
+
+ return err
+}
diff --git a/pkg/log/file_test.go b/pkg/log/file_test.go
index 458a39d754b..3e98e0786cc 100644
--- a/pkg/log/file_test.go
+++ b/pkg/log/file_test.go
@@ -38,6 +38,7 @@ func TestLogFile(t *testing.T) {
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
})
+ fileLogWrite.Close()
err = os.Remove(fileLogWrite.Filename)
So(err, ShouldBeNil)
})
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index b7f83404452..68054fab5ef 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -366,7 +366,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"steppedLine": false,
"targets": [
{
- "dsType": "influxdb",
"groupBy": [
{
"params": [
@@ -411,7 +410,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"tags": []
},
{
- "dsType": "influxdb",
"groupBy": [
{
"params": [
diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index ed1451da419..e051a71740a 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -6,6 +6,7 @@ import (
"io"
"mime/multipart"
"os"
+ "path/filepath"
"time"
"github.com/grafana/grafana/pkg/bus"
@@ -176,7 +177,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
if evalContext.ImageOnDiskPath == "" {
- evalContext.ImageOnDiskPath = "public/img/mixed_styles.png"
+ evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png")
}
log.Info("Uploading to slack via file.upload API")
headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index 110553d3b05..669f655a159 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -401,7 +401,7 @@ func SearchUsers(query *m.SearchUsersQuery) error {
}
if query.Query != "" {
- whereConditions = append(whereConditions, "(email LIKE ? OR name LIKE ? OR login like ?)")
+ whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)")
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
}
diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go
index 7cadf055ff6..73b173813af 100644
--- a/pkg/tsdb/graphite/graphite.go
+++ b/pkg/tsdb/graphite/graphite.go
@@ -17,7 +17,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
- opentracing "github.com/opentracing/opentracing-go"
+ "github.com/opentracing/opentracing-go"
)
type GraphiteExecutor struct {
@@ -158,7 +158,7 @@ func formatTimeRange(input string) string {
if input == "now" {
return input
}
- return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
+ return strings.Replace(strings.Replace(strings.Replace(input, "now", "", -1), "m", "min", -1), "M", "mon", -1)
}
func fixIntervalFormat(target string) string {
diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go
index c1a2736293b..1704a9b5f55 100644
--- a/pkg/tsdb/graphite/graphite_test.go
+++ b/pkg/tsdb/graphite/graphite_test.go
@@ -18,14 +18,14 @@ func TestGraphiteFunctions(t *testing.T) {
Convey("formatting time range for now-1m", func() {
timeRange := formatTimeRange("now-1m")
- So(timeRange, ShouldEqual, "now-1min")
+ So(timeRange, ShouldEqual, "-1min")
})
Convey("formatting time range for now-1M", func() {
timeRange := formatTimeRange("now-1M")
- So(timeRange, ShouldEqual, "now-1mon")
+ So(timeRange, ShouldEqual, "-1mon")
})
diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go
index f8759afd3ba..7be9cae9702 100644
--- a/pkg/tsdb/influxdb/model_parser_test.go
+++ b/pkg/tsdb/influxdb/model_parser_test.go
@@ -20,7 +20,6 @@ func TestInfluxdbQueryParser(t *testing.T) {
Convey("can parse influxdb json model", func() {
json := `
{
- "dsType": "influxdb",
"groupBy": [
{
"params": [
@@ -123,7 +122,6 @@ func TestInfluxdbQueryParser(t *testing.T) {
Convey("can part raw query json model", func() {
json := `
{
- "dsType": "influxdb",
"groupBy": [
{
"params": [
diff --git a/pkg/tsdb/influxdb/response_parser.go b/pkg/tsdb/influxdb/response_parser.go
index b7db6182241..8de8dcbb464 100644
--- a/pkg/tsdb/influxdb/response_parser.go
+++ b/pkg/tsdb/influxdb/response_parser.go
@@ -50,6 +50,7 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul
result = append(result, &tsdb.TimeSeries{
Name: rp.formatSerieName(row, column, query),
Points: points,
+ Tags: row.Tags,
})
}
}
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index dcb60977edc..a8c96d8119c 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -78,6 +78,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
rowLimit := 1000000
rowCount := 0
+ timeIndex := -1
+
+ // check if there is a column named time
+ for i, col := range columnNames {
+ switch col {
+ case "time":
+ timeIndex = i
+ }
+ }
for ; rows.Next(); rowCount++ {
if rowCount > rowLimit {
@@ -89,6 +98,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
return err
}
+ // convert column named time to unix timestamp to make
+ // native datetime postgres types work in annotation queries
+ if timeIndex != -1 {
+ switch value := values[timeIndex].(type) {
+ case time.Time:
+ values[timeIndex] = float64(value.UnixNano() / 1e9)
+ }
+ }
+
table.Rows = append(table.Rows, values)
}
@@ -142,8 +160,13 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
pointsBySeries := make(map[string]*tsdb.TimeSeries)
seriesByQueryOrder := list.New()
- columnNames, err := rows.Columns()
+ columnNames, err := rows.Columns()
+ if err != nil {
+ return err
+ }
+
+ columnTypes, err := rows.ColumnTypes()
if err != nil {
return err
}
@@ -153,13 +176,21 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
timeIndex := -1
metricIndex := -1
- // check columns of resultset
+ // check columns of resultset: a column named time is mandatory
+ // the first text column is treated as metric name unless a column named metric is present
for i, col := range columnNames {
switch col {
case "time":
timeIndex = i
case "metric":
metricIndex = i
+ default:
+ if metricIndex == -1 {
+ switch columnTypes[i].DatabaseTypeName() {
+ case "UNKNOWN", "TEXT", "VARCHAR", "CHAR":
+ metricIndex = i
+ }
+ }
}
}
diff --git a/pkg/util/url.go b/pkg/util/url.go
index ba452596a2b..c82dcef67c5 100644
--- a/pkg/util/url.go
+++ b/pkg/util/url.go
@@ -9,10 +9,15 @@ type UrlQueryReader struct {
values url.Values
}
-func NewUrlQueryReader(url *url.URL) *UrlQueryReader {
- return &UrlQueryReader{
- values: url.Query(),
+func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) {
+ u, err := url.ParseQuery(urlInfo.String())
+ if err != nil {
+ return nil, err
}
+
+ return &UrlQueryReader{
+ values: u,
+ }, nil
}
func (r *UrlQueryReader) Get(name string, def string) string {
diff --git a/public/app/app.ts b/public/app/app.ts
index 8e345c6abed..b76dceb4943 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -9,7 +9,6 @@ import 'angular-native-dragdrop';
import 'angular-bindonce';
import 'react';
import 'react-dom';
-import 'ngreact';
import 'vendor/bootstrap/bootstrap';
import 'vendor/angular-ui/ui-bootstrap-tpls';
diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts
index 00181736119..4cb3ca513cc 100644
--- a/public/app/core/angular_wrappers.ts
+++ b/public/app/core/angular_wrappers.ts
@@ -1,10 +1,10 @@
import { react2AngularDirective } from 'app/core/utils/react2angular';
import { PasswordStrength } from './components/PasswordStrength';
-import PageHeader from './components/PageHeader';
+import PageHeader from './components/PageHeader/PageHeader';
+import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
export function registerAngularDirectives() {
-
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
- react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]);
-
+ react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
+ react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
}
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx
new file mode 100644
index 00000000000..d62ae892a0a
--- /dev/null
+++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import EmptyListCTA from './EmptyListCTA';
+
+const model = {
+ title: 'Title',
+ buttonIcon: 'ga css class',
+ buttonLink: 'http://url/to/destination',
+ buttonTitle: 'Click me',
+ proTip: 'This is a tip',
+ proTipLink: 'http://url/to/tip/destination',
+ proTipLinkTitle: 'Learn more',
+ proTipTarget: '_blank'
+};
+
+describe('CollorPalette', () => {
+
+ it('renders correctly', () => {
+ const tree = renderer.create().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
new file mode 100644
index 00000000000..a5a78a63bf1
--- /dev/null
+++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
@@ -0,0 +1,34 @@
+import React, { Component } from 'react';
+
+export interface IProps {
+ model: any;
+}
+
+class EmptyListCTA extends Component {
+ render() {
+ const {
+ title,
+ buttonIcon,
+ buttonLink,
+ buttonTitle,
+ proTip,
+ proTipLink,
+ proTipLinkTitle,
+ proTipTarget
+ } = this.props.model;
+ return (
+
\ No newline at end of file
diff --git a/public/app/features/dashboard/partials/folder_permissions.html b/public/app/features/dashboard/partials/folder_permissions.html
new file mode 100644
index 00000000000..ad34618457d
--- /dev/null
+++ b/public/app/features/dashboard/partials/folder_permissions.html
@@ -0,0 +1,5 @@
+
+
+
+
Coming soon! Permissions will be added in Grafana 5.0 beta.
+
\ No newline at end of file
diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/settings/settings.html
index 0781b0c109d..81508c6ec68 100644
--- a/public/app/features/dashboard/settings/settings.html
+++ b/public/app/features/dashboard/settings/settings.html
@@ -1,5 +1,5 @@
-
+ Legend format
+
+
+
+ Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
+ the label hostname.
+
+
-
- Min step
-
-
- Leave blank for auto handling based on time range and panel width
-
-
+
+ Min step
+
+
+ Leave blank for auto handling based on time range and panel width
+
+
';
- tableHeaderElem = $(header);
- }
-
- if (panel.legend.sort) {
- seriesList = _.sortBy(seriesList, function(series) {
- return series.stats[panel.legend.sort];
- });
- if (panel.legend.sortDesc) {
- seriesList = seriesList.reverse();
- }
- }
-
- var seriesShown = 0;
- var seriesElements = [];
-
- for (i = 0; i < seriesList.length; i++) {
- var series = seriesList[i];
-
- if (series.hideFromLegend(panel.legend)) {
- continue;
- }
-
- var html = '
';
- html += '
';
- html += '';
- html += '
';
-
- html += '' + series.aliasEscaped + '';
-
- if (panel.legend.values) {
- var avg = series.formatValue(series.stats.avg);
- var current = series.formatValue(series.stats.current);
- var min = series.formatValue(series.stats.min);
- var max = series.formatValue(series.stats.max);
- var total = series.formatValue(series.stats.total);
-
- if (panel.legend.min) { html += '
' + min + '
'; }
- if (panel.legend.max) { html += '
' + max + '
'; }
- if (panel.legend.avg) { html += '
' + avg + '
'; }
- if (panel.legend.current) { html += '
' + current + '
'; }
- if (panel.legend.total) { html += '
' + total + '
'; }
- }
-
- html += '
';
- seriesElements.push($(html));
-
- seriesShown++;
- }
-
- if (panel.legend.alignAsTable) {
- var maxHeight = ctrl.height;
-
- if (!panel.legend.rightSide) {
- maxHeight = maxHeight/2;
- }
-
- var topPadding = 6;
- var tbodyElem = $('');
- tbodyElem.css("max-height", maxHeight - topPadding);
- tbodyElem.append(tableHeaderElem);
- tbodyElem.append(seriesElements);
- $container.append(tbodyElem);
- } else {
- $container.append(seriesElements);
- }
- }
- }
- };
- });
-
-});
diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts
new file mode 100644
index 00000000000..1a445068b97
--- /dev/null
+++ b/public/app/plugins/panel/graph/legend.ts
@@ -0,0 +1,233 @@
+import angular from 'angular';
+import _ from 'lodash';
+import $ from 'jquery';
+import PerfectScrollbar from 'perfect-scrollbar';
+import {updateLegendValues} from 'app/core/core';
+
+var module = angular.module('grafana.directives');
+
+module.directive('graphLegend', function(popoverSrv, $timeout) {
+
+ return {
+ link: function(scope, elem) {
+ var firstRender = true;
+ var ctrl = scope.ctrl;
+ var panel = ctrl.panel;
+ var data;
+ var seriesList;
+ var i;
+ var legendScrollbar;
+
+ scope.$on("$destroy", function() {
+ if (legendScrollbar) {
+ legendScrollbar.destroy();
+ }
+ });
+
+ ctrl.events.on('render-legend', () => {
+ data = ctrl.seriesList;
+ if (data) {
+ render();
+ }
+ ctrl.events.emit('legend-rendering-complete');
+ });
+
+ function updateLegendDecimals() {
+ updateLegendValues(data, panel);
+ }
+
+ function getSeriesIndexForElement(el) {
+ return el.parents('[data-series-index]').data('series-index');
+ }
+
+ function openColorSelector(e) {
+ // if we clicked inside poup container ignore click
+ if ($(e.target).parents('.popover').length) {
+ return;
+ }
+
+ var el = $(e.currentTarget).find('.fa-minus');
+ var index = getSeriesIndexForElement(el);
+ var series = seriesList[index];
+
+ $timeout(function() {
+ popoverSrv.show({
+ element: el[0],
+ position: 'bottom center',
+ template: '' +
+ '',
+ openOn: 'hover',
+ model: {
+ series: series,
+ toggleAxis: function() {
+ ctrl.toggleAxis(series);
+ },
+ colorSelected: function(color) {
+ ctrl.changeSeriesColor(series, color);
+ }
+ },
+ });
+ });
+ }
+
+ function toggleSeries(e) {
+ var el = $(e.currentTarget);
+ var index = getSeriesIndexForElement(el);
+ var seriesInfo = seriesList[index];
+ var scrollPosition = $(elem.children('tbody')).scrollTop();
+ ctrl.toggleSeries(seriesInfo, e);
+ $(elem.children('tbody')).scrollTop(scrollPosition);
+ }
+
+ function sortLegend(e) {
+ var el = $(e.currentTarget);
+ var stat = el.data('stat');
+
+ if (stat !== panel.legend.sort) { panel.legend.sortDesc = null; }
+
+ // if already sort ascending, disable sorting
+ if (panel.legend.sortDesc === false) {
+ panel.legend.sort = null;
+ panel.legend.sortDesc = null;
+ ctrl.render();
+ return;
+ }
+
+ panel.legend.sortDesc = !panel.legend.sortDesc;
+ panel.legend.sort = stat;
+ ctrl.render();
+ }
+
+ function getTableHeaderHtml(statName) {
+ if (!panel.legend[statName]) { return ""; }
+ var html = '
' + statName;
+
+ if (panel.legend.sort === statName) {
+ var cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up' ;
+ html += ' ';
+ }
+
+ return html + '
';
+ }
+
+ function render() {
+ if (!ctrl.panel.legend.show) {
+ elem.empty();
+ firstRender = true;
+ return;
+ }
+
+ if (firstRender) {
+ elem.on('click', '.graph-legend-icon', openColorSelector);
+ elem.on('click', '.graph-legend-alias', toggleSeries);
+ elem.on('click', 'th', sortLegend);
+ firstRender = false;
+ }
+
+ seriesList = data;
+
+ elem.empty();
+
+ // Set min-width if side style and there is a value, otherwise remove the CSS propery
+ var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : "";
+ elem.css("min-width", width);
+
+ elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
+
+ var tableHeaderElem;
+ if (panel.legend.alignAsTable) {
+ var header = '
';
+ tableHeaderElem = $(header);
+ }
+
+ if (panel.legend.sort) {
+ seriesList = _.sortBy(seriesList, function(series) {
+ return series.stats[panel.legend.sort];
+ });
+ if (panel.legend.sortDesc) {
+ seriesList = seriesList.reverse();
+ }
+ }
+
+ // render first time for getting proper legend height
+ if (!panel.legend.rightSide) {
+ renderLegendElement(tableHeaderElem);
+ updateLegendDecimals();
+ elem.empty();
+ } else {
+ updateLegendDecimals();
+ }
+
+ renderLegendElement(tableHeaderElem);
+ }
+
+ function renderSeriesLegendElements() {
+ let seriesElements = [];
+ for (i = 0; i < seriesList.length; i++) {
+ var series = seriesList[i];
+
+ if (series.hideFromLegend(panel.legend)) {
+ continue;
+ }
+
+ var html = '
';
+ html += '
';
+ html += '';
+ html += '
';
+
+ html += '' + series.aliasEscaped + '';
+
+ if (panel.legend.values) {
+ var avg = series.formatValue(series.stats.avg);
+ var current = series.formatValue(series.stats.current);
+ var min = series.formatValue(series.stats.min);
+ var max = series.formatValue(series.stats.max);
+ var total = series.formatValue(series.stats.total);
+
+ if (panel.legend.min) { html += '