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 ( +
+
{title}
+ {buttonTitle} +
+ ProTip: {proTip} + {proTipLinkTitle} +
+
+ ); + } +} + +export default EmptyListCTA; diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap new file mode 100644 index 00000000000..0598d7ecd4e --- /dev/null +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollorPalette renders correctly 1`] = ` +
+
+ Title +
+ + + Click me + +
+ + ProTip: + This is a tip + + Learn more + +
+
+`; diff --git a/public/app/core/components/PageHeader.tsx b/public/app/core/components/PageHeader.tsx deleted file mode 100644 index e0accc03fd4..00000000000 --- a/public/app/core/components/PageHeader.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { NavModel, NavModelItem } from '../nav_model_srv'; -import classNames from 'classnames'; - -export interface IProps { - model: NavModel; -} - -function TabItem(tab: NavModelItem) { - if (tab.hideFromTabs) { - return (null); - } - - let tabClasses = classNames({ - 'gf-tabs-link': true, - active: tab.active, - }); - - return ( -
  • - - - {tab.text} - -
  • - ); -} - -function Tabs({main}: {main: NavModelItem}) { - return
      {main.children.map(TabItem)}
    ; -} - -export default class PageHeader extends React.Component { - constructor(props) { - super(props); - } - - renderHeaderTitle(main) { - return ( -
    - - {main.icon && } - {main.img && } - - -
    -

    {main.text}

    - {main.subTitle &&
    {main.subTitle}
    } - {main.subType && ( -
    - - {main.subType.text} -
    - )} -
    -
    - ); - } - - render() { - return ( -
    -
    -
    - {this.renderHeaderTitle(this.props.model.main)} - {this.props.model.main.children && } -
    -
    -
    - ); - } -} diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx new file mode 100644 index 00000000000..3ab6cac8143 --- /dev/null +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { NavModel, NavModelItem } from '../../nav_model_srv'; +import classNames from 'classnames'; +import appEvents from 'app/core/app_events'; + +export interface IProps { + model: NavModel; +} + +function TabItem(tab: NavModelItem) { + if (tab.hideFromTabs) { + return (null); + } + + let tabClasses = classNames({ + 'gf-tabs-link': true, + active: tab.active, + }); + + return ( +
  • + + + {tab.text} + +
  • + ); +} + +function SelectOption(navItem: NavModelItem) { + if (navItem.hideFromTabs) { // TODO: Rename hideFromTabs => hideFromNav + return (null); + } + + return ( + + ); +} + +function Navigation({main}: {main: NavModelItem}) { + return (); +} + +function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) { + const defaultSelectedItem = main.children.find(navItem => { + return navItem.active === true; + }); + + const gotoUrl = evt => { + var element = evt.target; + var url = element.options[element.selectedIndex].value; + appEvents.emit('location-change', {href: url}); + }; + + return (); +} + +function Tabs({main, customCss}: {main: NavModelItem, customCss: string}) { + return
      {main.children.map(TabItem)}
    ; +} + +export default class PageHeader extends React.Component { + constructor(props) { + super(props); + } + + renderBreadcrumb(breadcrumbs) { + const breadcrumbsResult = []; + for (let i = 0; i < breadcrumbs.length; i++) { + const bc = breadcrumbs[i]; + if (bc.uri) { + breadcrumbsResult.push({bc.title}); + } else { + breadcrumbsResult.push( / {bc.title}); + } + } + return breadcrumbsResult; + } + + renderHeaderTitle(main) { + return ( +
    + + {main.icon && } + {main.img && } + + +
    + {main.text &&

    {main.text}

    } + {main.breadcrumbs && main.breadcrumbs.length > 0 && ( +

    + {this.renderBreadcrumb(main.breadcrumbs)} +

    ) + } + {main.subTitle &&
    {main.subTitle}
    } + {main.subType && ( +
    + + {main.subType.text} +
    + )} +
    +
    + ); + } + + render() { + return ( +
    +
    +
    + {this.renderHeaderTitle(this.props.model.main)} + {this.props.model.main.children && } +
    +
    +
    + ); + } +} diff --git a/public/app/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx new file mode 100644 index 00000000000..49a200b0f3b --- /dev/null +++ b/public/app/core/components/ScrollBar/ScrollBar.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import PerfectScrollbar from 'perfect-scrollbar'; + +export interface Props { + children: any; + className: string; +} + +export default class ScrollBar extends React.Component { + + private container: any; + private ps: PerfectScrollbar; + + constructor(props) { + super(props); + } + + componentDidMount() { + this.ps = new PerfectScrollbar(this.container); + } + + componentDidUpdate() { + this.ps.update(); + } + + componentWillUnmount() { + this.ps.destroy(); + } + + // methods can be invoked by outside + setScrollTop(top) { + if (this.container) { + this.container.scrollTop = top; + this.ps.update(); + + return true; + } + return false; + } + + setScrollLeft(left) { + if (this.container) { + this.container.scrollLeft = left; + this.ps.update(); + + return true; + } + return false; + } + + handleRef = ref => { + this.container = ref; + }; + + render() { + return ( +
    + {this.props.children} +
    + ); + } +} diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index d13c6a6a00e..eff55405d6f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -1,5 +1,3 @@ -/// - import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; @@ -12,7 +10,7 @@ import Drop from 'tether-drop'; export class GrafanaCtrl { /** @ngInject */ - constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv) { + constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) { $scope.init = function() { $scope.contextSrv = contextSrv; @@ -23,6 +21,7 @@ export class GrafanaCtrl { profiler.init(config, $rootScope); alertSrv.init(); utilSrv.init(); + globalEventSrv.init(); $scope.dashAlerts = alertSrv; }; @@ -78,11 +77,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop sidemenuOpen = scope.contextSrv.sidemenu; body.toggleClass('sidemenu-open', sidemenuOpen); - scope.$watch('contextSrv.sidemenu', newVal => { - if (sidemenuOpen !== scope.contextSrv.sidemenu) { - sidemenuOpen = scope.contextSrv.sidemenu; - body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu); - } + appEvents.on('toggle-sidemenu', () => { + body.toggleClass('sidemenu-open'); + }); + + appEvents.on('toggle-sidemenu-mobile', () => { + body.toggleClass('sidemenu-open--xs'); + }); + + appEvents.on('toggle-sidemenu-hidden', () => { + body.toggleClass('sidemenu-hidden'); }); // tooltip removal fix @@ -100,6 +104,9 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } } + // clear body class sidemenu states + body.removeClass('sidemenu-open--xs'); + $("#tooltip, .tooltip").remove(); // check for kiosk url param diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html new file mode 100644 index 00000000000..957bca0262c --- /dev/null +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -0,0 +1,98 @@ +
    +
    + + +
    +
    + + + Dashboard + + + + Folder + +
    + +
    + Filters: + + + + {{tagName}} + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + +
    \ No newline at end of file diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts new file mode 100644 index 00000000000..5b74a0e50fc --- /dev/null +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -0,0 +1,221 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import { SearchSrv } from 'app/core/services/search_srv'; + +export class ManageDashboardsCtrl { + public sections: any[]; + tagFilterOptions: any[]; + selectedTagFilter: any; + query: any; + navModel: any; + canDelete = false; + canMove = false; + hasFilters = false; + selectAllChecked = false; + starredFilterOptions = [{ text: 'Filter by Starred', disabled: true }, { text: 'Yes' }, { text: 'No' }]; + selectedStarredFilter: any; + folderId?: number; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) { + this.query = { query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true }; + + if (this.folderId) { + this.query.folderIds = [this.folderId]; + } + + this.selectedStarredFilter = this.starredFilterOptions[0]; + + this.getDashboards().then(() => { + this.getTags(); + }); + } + + getDashboards() { + return this.searchSrv.search(this.query).then((result) => { + return this.initDashboardList(result); + }); + } + + initDashboardList(result: any) { + this.canMove = false; + this.canDelete = false; + this.selectAllChecked = false; + this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred; + + if (!result) { + this.sections = []; + return; + } + + this.sections = result; + + for (let section of this.sections) { + section.checked = false; + + for (let dashboard of section.items) { + dashboard.checked = false; + } + } + } + + selectionChanged() { + + let selectedDashboards = 0; + + for (let section of this.sections) { + selectedDashboards += _.filter(section.items, { checked: true }).length; + } + + const selectedFolders = _.filter(this.sections, { checked: true }).length; + this.canMove = selectedDashboards > 0 && selectedFolders === 0; + this.canDelete = selectedDashboards > 0 || selectedFolders > 0; + } + + getDashboardsToDelete() { + let selectedDashboards = []; + + for (const section of this.sections) { + if (section.checked) { + selectedDashboards.push(section.uri); + } else { + const selected = _.filter(section.items, { checked: true }); + selectedDashboards.push(..._.map(selected, 'uri')); + } + } + + return selectedDashboards; + } + + getFolderIds(sections) { + const ids = []; + for (let s of sections) { + if (s.checked) { + ids.push(s.id); + } + } + return ids; + } + + delete() { + const selectedDashboards = this.getDashboardsToDelete(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete the ${selectedDashboards.length} selected dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + const promises = []; + for (let dash of selectedDashboards) { + promises.push(this.backendSrv.delete(`/api/dashboards/${dash}`)); + } + + this.$q.all(promises).then(() => { + this.getDashboards(); + }); + } + }); + } + + getDashboardsToMove() { + let selectedDashboards = []; + + for (const section of this.sections) { + const selected = _.filter(section.items, { checked: true }); + selectedDashboards.push(..._.map(selected, 'uri')); + } + + return selectedDashboards; + } + + moveTo() { + const selectedDashboards = this.getDashboardsToMove(); + + const template = '' + + '`'; + appEvents.emit('show-modal', { + templateHtml: template, + modalClass: 'modal--narrow', + model: { dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this) } + }); + } + + getTags() { + return this.searchSrv.getDashboardTags().then((results) => { + this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); + this.selectedTagFilter = this.tagFilterOptions[0]; + }); + } + + filterByTag(tag) { + if (_.indexOf(this.query.tag, tag) === -1) { + this.query.tag.push(tag); + } + + return this.getDashboards(); + } + + onQueryChange() { + return this.getDashboards(); + } + + onTagFilterChange() { + var res = this.filterByTag(this.selectedTagFilter.term); + this.selectedTagFilter = this.tagFilterOptions[0]; + return res; + } + + removeTag(tag, evt) { + this.query.tag = _.without(this.query.tag, tag); + this.getDashboards(); + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + onStarredFilterChange() { + this.query.starred = this.selectedStarredFilter.text === 'Yes'; + return this.getDashboards(); + } + + onSelectAllChanged() { + for (let section of this.sections) { + if (!section.hideHeader) { + section.checked = this.selectAllChecked; + } + + section.items = _.map(section.items, (item) => { + item.checked = this.selectAllChecked; + return item; + }); + } + + this.selectionChanged(); + } + + clearFilters() { + this.query.query = ''; + this.query.tag = []; + this.query.starred = false; + this.getDashboards(); + } +} + +export function manageDashboardsDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/core/components/manage_dashboards/manage_dashboards.html', + controller: ManageDashboardsCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + folderId: '=' + } + }; +} + +coreModule.directive('manageDashboards', manageDashboardsDirective); diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 811e21bc64a..f9b953efcee 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -20,37 +20,14 @@
    -
    -
    No dashboards matching your query were found.
    - -
    diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 728b6a3ee18..ffbec30cc0a 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -64,18 +64,75 @@ export class SearchCtrl { this.moveSelection(-1); } if (evt.keyCode === 13) { - var selectedDash = this.results[this.selectedIndex]; - if (selectedDash) { - this.$location.search({}); - this.$location.path(selectedDash.url); + const flattenedResult = this.getFlattenedResultForNavigation(); + const currentItem = flattenedResult[this.selectedIndex]; + + if (currentItem) { + if (currentItem.dashboardIndex !== undefined) { + const selectedDash = this.results[currentItem.folderIndex].items[currentItem.dashboardIndex]; + + if (selectedDash) { + this.$location.search({}); + this.$location.path(selectedDash.url); + } + } else { + const selectedFolder = this.results[currentItem.folderIndex]; + + if (selectedFolder) { + selectedFolder.toggle(selectedFolder); + } + } } } } moveSelection(direction) { - var max = (this.results || []).length; - var newIndex = this.selectedIndex + direction; + if (this.results.length === 0) { + return; + } + + const flattenedResult = this.getFlattenedResultForNavigation(); + const currentItem = flattenedResult[this.selectedIndex]; + + if (currentItem) { + if (currentItem.dashboardIndex !== undefined) { + this.results[currentItem.folderIndex].items[currentItem.dashboardIndex].selected = false; + } else { + this.results[currentItem.folderIndex].selected = false; + } + } + + if (direction === 0) { + this.selectedIndex = -1; + return; + } + + const max = flattenedResult.length; + let newIndex = this.selectedIndex + direction; this.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex; + const selectedItem = flattenedResult[this.selectedIndex]; + + if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) { + this.moveSelection(direction); + return; + } + + if (selectedItem.dashboardIndex !== undefined) { + if (!this.results[selectedItem.folderIndex].expanded) { + this.moveSelection(direction); + return; + } + + this.results[selectedItem.folderIndex].items[selectedItem.dashboardIndex].selected = true; + return; + } + + if (this.results[selectedItem.folderIndex].hideHeader) { + this.moveSelection(direction); + return; + } + + this.results[selectedItem.folderIndex].selected = true; } searchDashboards() { @@ -84,8 +141,9 @@ export class SearchCtrl { return this.searchSrv.search(this.query).then(results => { if (localSearchId < this.currentSearchId) { return; } - this.results = results; + this.results = results || []; this.isLoading = false; + this.moveSelection(1); }); } @@ -94,13 +152,11 @@ export class SearchCtrl { return query.query === '' && query.starred === false && query.tag.length === 0; } - filterByTag(tag, evt) { - this.query.tag.push(tag); - this.search(); - this.giveSearchFocus = this.giveSearchFocus + 1; - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); + filterByTag(tag) { + if (_.indexOf(this.query.tag, tag) === -1) { + this.query.tag.push(tag); + this.search(); + this.giveSearchFocus = this.giveSearchFocus + 1; } } @@ -127,12 +183,36 @@ export class SearchCtrl { search() { this.showImport = false; - this.selectedIndex = 0; + this.selectedIndex = -1; this.searchDashboards(); } - toggleFolder(section) { - this.searchSrv.toggleSection(section); + folderExpanding() { + this.moveSelection(0); + } + + private getFlattenedResultForNavigation() { + let folderIndex = 0; + + return _.flatMap(this.results, (s) => { + let result = []; + + result.push({ + folderIndex: folderIndex + }); + + let dashboardIndex = 0; + + result = result.concat(_.map(s.items || [], (i) => { + return { + folderIndex: folderIndex, + dashboardIndex: dashboardIndex++ + }; + })); + + folderIndex++; + return result; + }); } } diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html new file mode 100644 index 00000000000..ad0aa3e6b71 --- /dev/null +++ b/public/app/core/components/search/search_results.html @@ -0,0 +1,47 @@ + \ No newline at end of file diff --git a/public/app/core/components/search/search_results.ts b/public/app/core/components/search/search_results.ts new file mode 100644 index 00000000000..02b6c9f355b --- /dev/null +++ b/public/app/core/components/search/search_results.ts @@ -0,0 +1,76 @@ +// import _ from 'lodash'; +import coreModule from '../../core_module'; + +export class SearchResultsCtrl { + results: any; + onSelectionChanged: any; + onTagSelected: any; + onFolderExpanding: any; + + /** @ngInject */ + constructor(private $location) { + + } + + toggleFolderExpand(section) { + if (section.toggle) { + if (!section.expanded && this.onFolderExpanding) { + this.onFolderExpanding(); + } + + section.toggle(section); + } + } + + navigateToFolder(section, evt) { + this.$location.path('/dashboards/folder/' + section.id + '/' + section.uri); + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + toggleSelection(item, evt) { + item.checked = !item.checked; + + if (this.onSelectionChanged) { + this.onSelectionChanged(); + } + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + selectTag(tag, evt) { + if (this.onTagSelected) { + this.onTagSelected({$tag: tag}); + } + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } +} + +export function searchResultsDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/core/components/search/search_results.html', + controller: SearchResultsCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + editable: '@', + results: '=', + onSelectionChanged: '&', + onTagSelected: '&', + onFolderExpanding: '&' + }, + }; +} + +coreModule.directive('dashboardSearchResults', searchResultsDirective); diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 2869562c5b1..e97d34739ba 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -2,6 +2,11 @@ + + +  Close + +
    -
    + {this.state.panelPlugins.map(this.renderPanelItem.bind(this))} -
    +
    ); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 283bf0c74d3..7b0f040e199 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -11,7 +11,7 @@ import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; -function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) { +function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange, className}) { if (size.width === 0) { console.log('size is zero!'); } @@ -25,12 +25,12 @@ function GridWrapper({size, layout, onLayoutChange, children, onResize, onResize return ( { this.onResizeStop = this.onResizeStop.bind(this); this.onWidthChange = this.onWidthChange.bind(this); + this.state = {animated: false}; + // subscribe to dashboard events this.dashboard = this.panelContainer.getDashboard(); this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); @@ -134,6 +136,14 @@ export class DashboardGrid extends React.Component { this.panelMap[newItem.i].resizeDone(); } + componentDidMount() { + setTimeout(() => { + this.setState(() => { + return {animated: true}; + }); + }); + } + renderPanels() { const panelElements = []; @@ -152,6 +162,7 @@ export class DashboardGrid extends React.Component { render() { return ( { + const folderTitle = result.dashboard.title; + navModel.main.text = ''; + navModel.main.breadcrumbs = [ + { title: 'Dashboards', uri: '/dashboards' }, + { title: folderTitle } + ]; + const folderUrl = `/dashboards/folder/${folderId}/${result.meta.type}/${result.meta.slug}`; + const dashTab = _.find(navModel.main.children, { id: 'manage-folder-dashboards' }); + dashTab.url = folderUrl; + const permTab = _.find(navModel.main.children, { id: 'manage-folder-permissions' }); + permTab.url = folderUrl + '/permissions'; + }); + } +} diff --git a/public/app/features/dashboard/folder_permissions_ctrl.ts b/public/app/features/dashboard/folder_permissions_ctrl.ts new file mode 100644 index 00000000000..a7d46088ac5 --- /dev/null +++ b/public/app/features/dashboard/folder_permissions_ctrl.ts @@ -0,0 +1,16 @@ +import {FolderPageLoader} from './folder_page_loader'; + +export class FolderPermissionsCtrl { + navModel: any; + folderId: number; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $routeParams) { + if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) { + this.folderId = $routeParams.folderId; + this.navModel = navModelSrv.getNav('manage-folder', 'manage-folder-permissions', 0); + + new FolderPageLoader(this.backendSrv, this.$routeParams).load(this.navModel, this.folderId); + } + } +} diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html deleted file mode 100644 index c617c752366..00000000000 --- a/public/app/features/dashboard/import/dash_import.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - -
    - diff --git a/public/app/features/dashboard/partials/create_folder.html b/public/app/features/dashboard/partials/create_folder.html new file mode 100644 index 00000000000..ae5fcc126e8 --- /dev/null +++ b/public/app/features/dashboard/partials/create_folder.html @@ -0,0 +1,43 @@ + + +
    + + + +
    diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html new file mode 100644 index 00000000000..b740b8f38bc --- /dev/null +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -0,0 +1,126 @@ + + +
    +
    + +
    +
    + +
    + +
    Grafana.com Dashboard
    + +
    +
    + +
    +
    + +
    +
    + +
    Or paste JSON
    + +
    +
    + +
    + + + + {{ctrl.parseError}} + +
    +
    + +
    +
    +

    + Importing Dashboard from + Grafana.com +

    + +
    + + +
    +
    + + +
    +
    + +

    + Options +

    + +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    + +
    + + + +
    +
    +
    + +
    + + + Cancel +
    + +
    +
    diff --git a/public/app/features/dashboard/partials/dashboardList.html b/public/app/features/dashboard/partials/dashboardList.html deleted file mode 100644 index 8bb46ebd127..00000000000 --- a/public/app/features/dashboard/partials/dashboardList.html +++ /dev/null @@ -1,131 +0,0 @@ - - -
    -
    -
    - - -
    -
    - - - Dashboard - - - - Folder - -
    - -
    - Filters: - - - - {{tagName}} - - -
    - -
    -
    - -
    -
    - -
    -
    - - -
    -
    - - -
    - - - No Dashboards or Folders found. - diff --git a/public/app/features/dashboard/partials/dashboard_list.html b/public/app/features/dashboard/partials/dashboard_list.html new file mode 100644 index 00000000000..241d464434a --- /dev/null +++ b/public/app/features/dashboard/partials/dashboard_list.html @@ -0,0 +1,5 @@ + + +
    + +
    diff --git a/public/app/features/dashboard/partials/folder_dashboards.html b/public/app/features/dashboard/partials/folder_dashboards.html new file mode 100644 index 00000000000..eeda28ad4a4 --- /dev/null +++ b/public/app/features/dashboard/partials/folder_dashboards.html @@ -0,0 +1,5 @@ + + +
    + +
    \ 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 @@ -
    +
    +
    +
    diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.ts similarity index 87% rename from public/app/features/dashboard/shareModalCtrl.js rename to public/app/features/dashboard/shareModalCtrl.ts index e750b631771..1ddec697eb2 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -1,18 +1,11 @@ -define(['angular', - 'lodash', - 'jquery', - 'moment', - 'app/core/config', -], -function (angular, _, $, moment, config) { - 'use strict'; +import angular from 'angular'; +import moment from 'moment'; +import config from 'app/core/config'; - config = config.default; - - var module = angular.module('grafana.controllers'); - - module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { +export class ShareModalCtrl { + /** @ngInject */ + constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' }; $scope.editor = { index: $scope.tabIndex || 0}; @@ -93,7 +86,7 @@ function (angular, _, $, moment, config) { $scope.getShareUrl = function() { return $scope.shareUrl; }; + } +} - }); - -}); +angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl); diff --git a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts similarity index 52% rename from public/app/features/dashboard/specs/dash_import_ctrl_specs.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts index c541aca34b2..3270bb5a732 100644 --- a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts @@ -1,25 +1,24 @@ -import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; +import {DashboardImportCtrl} from '../dashboard_import_ctrl'; +import config from '../../../core/config'; -import {DashImportCtrl} from 'app/features/dashboard/import/dash_import'; -import config from 'app/core/config'; - -describe('DashImportCtrl', function() { +describe('DashboardImportCtrl', function() { var ctx: any = {}; - var backendSrv = { - search: sinon.stub().returns(Promise.resolve([])), - get: sinon.stub() - }; - beforeEach(angularMocks.module('grafana.core')); + let navModelSrv; + let backendSrv; - beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { - ctx.$q = $q; - ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashImportCtrl, { - $scope: ctx.scope, - backendSrv: backendSrv, - }); - })); + beforeEach(() => { + navModelSrv = { + getNav: () => {} + }; + + backendSrv = { + search: jest.fn().mockReturnValue(Promise.resolve([])), + get: jest.fn() + }; + + ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {}); + }); describe('when uploading json', function() { beforeEach(function() { @@ -37,13 +36,13 @@ describe('DashImportCtrl', function() { }); it('should build input model', function() { - expect(ctx.ctrl.inputs.length).to.eql(1); - expect(ctx.ctrl.inputs[0].name).to.eql('ds'); - expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source'); + expect(ctx.ctrl.inputs.length).toBe(1); + expect(ctx.ctrl.inputs[0].name).toBe('ds'); + expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); }); it('should set inputValid to false', function() { - expect(ctx.ctrl.inputsValid).to.eql(false); + expect(ctx.ctrl.inputsValid).toBe(false); }); }); @@ -51,7 +50,7 @@ describe('DashImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -60,7 +59,7 @@ describe('DashImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); @@ -68,7 +67,7 @@ describe('DashImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = '2342'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -77,10 +76,8 @@ describe('DashImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); }); - - diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts index 2c8b5ebcd50..dfdcb7601a4 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { DashboardModel } from '../dashboard_model'; import { PanelModel } from '../panel_model'; import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); @@ -315,12 +316,33 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); + + it('should add repeated row if repeat set', function() { + model.rows = [ + createRow({showTitle: true, title: "Row", height: 8, repeat: "server"}, [[6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, + {x: 0, y: 1, w: 12, h: 8}, + {x: 0, y: 9, w: 24, h: 8}, + {x: 0, y: 10, w: 24, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + expect(dashboard.panels[0].repeat).toBe("server"); + expect(dashboard.panels[1].repeat).toBeUndefined(); + expect(dashboard.panels[2].repeat).toBeUndefined(); + expect(dashboard.panels[3].repeat).toBeUndefined(); + }); }); }); function createRow(options, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; - let {collapse, height, showTitle, title} = options; + let {collapse, height, showTitle, title, repeat} = options; height = height * PANEL_HEIGHT_STEP; let panels = []; _.each(panelDescriptions, panelDesc => { @@ -330,7 +352,7 @@ function createRow(options, panelDescriptions: any[]) { } panels.push(panel); }); - let row = {collapse, height, showTitle, title, panels}; + let row = {collapse, height, showTitle, title, panels, repeat}; return row; } diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 2ad1400990c..e6db5725d3d 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -1,4 +1,6 @@ +import _ from 'lodash'; import {DashboardModel} from '../dashboard_model'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({ @@ -146,19 +148,19 @@ describe('given dashboard with panel repeat in vertical direction', function() { }); }); -describe.skip('given dashboard with row repeat', function() { - var dashboard; +describe('given dashboard with row repeat', function() { + let dashboard, dashboardJSON; beforeEach(function() { - dashboard = new DashboardModel({ + dashboardJSON = { panels: [ - {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}}, + {id: 1, type: 'row', gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'}, {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, ], - templating: { + templating: { list: [{ name: 'apps', current: { @@ -172,33 +174,137 @@ describe.skip('given dashboard with row repeat', function() { ] }] } - }); + }; + dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); }); it('should not repeat only row', function() { - expect(dashboard.panels[1].type).toBe('graph'); + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual([ + 'row', 'graph', 'graph', + 'row', 'graph', 'graph', + 'row', 'graph' + ]); + }); + + it('should set scopedVars for each panel', function() { + dashboardJSON.templating.list[0].options[2].selected = true; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}}); + expect(dashboard.panels[4].scopedVars).toMatchObject({apps: {text: 'se2', value: 'se2'}}); + + const scopedVars = _.compact(_.map(dashboard.panels, (panel) => { + return panel.scopedVars ? panel.scopedVars.apps.value : null; + })); + + expect(scopedVars).toEqual([ + 'se1', 'se1', 'se1', + 'se2', 'se2', 'se2', + 'se3', 'se3', 'se3', + ]); + }); + + it('should repeat only configured row', function() { + expect(dashboard.panels[6].id).toBe(4); + expect(dashboard.panels[7].id).toBe(5); + }); + + it('should repeat only row if it is collapsed', function() { + dashboardJSON.panels = [ + { + id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}, + panels: [ + {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, + {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, + ] + }, + {id: 4, type: 'row', gridPos: {x: 0, y: 1, h: 1 , w: 24}}, + {id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}}, + ]; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual([ + 'row', 'row', 'row', 'graph' + ]); + expect(dashboard.panels[0].panels).toHaveLength(2); + expect(dashboard.panels[1].panels).toHaveLength(2); + }); + + it('should properly repeat multiple rows', function() { + dashboardJSON.panels = [ + {id: 1, type: 'row', gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'}, // repeat + {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, + {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, + {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, // don't touch + {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, + {id: 6, type: 'row', gridPos: {x: 0, y: 4, h: 1 , w: 24}, repeat: 'hosts'}, // repeat + {id: 7, type: 'graph', gridPos: {x: 0, y: 5, h: 1 , w: 6}}, + {id: 8, type: 'graph', gridPos: {x: 6, y: 5, h: 1 , w: 6}} + ]; + dashboardJSON.templating.list.push({ + name: 'hosts', + current: { + text: 'backend01, backend02', + value: ['backend01', 'backend02'] + }, + options: [ + {text: 'backend01', value: 'backend01', selected: true}, + {text: 'backend02', value: 'backend02', selected: true}, + {text: 'backend03', value: 'backend03', selected: false} + ] + }); + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual([ + 'row', 'graph', 'graph', + 'row', 'graph', 'graph', + 'row', 'graph', + 'row', 'graph', 'graph', + 'row', 'graph', 'graph', + ]); + + expect(dashboard.panels[0].scopedVars['apps'].value).toBe('se1'); + expect(dashboard.panels[1].scopedVars['apps'].value).toBe('se1'); + expect(dashboard.panels[3].scopedVars['apps'].value).toBe('se2'); + expect(dashboard.panels[4].scopedVars['apps'].value).toBe('se2'); + expect(dashboard.panels[8].scopedVars['hosts'].value).toBe('backend01'); + expect(dashboard.panels[9].scopedVars['hosts'].value).toBe('backend01'); + expect(dashboard.panels[11].scopedVars['hosts'].value).toBe('backend02'); + expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02'); + }); + + it('should assign unique ids for repeated panels', function() { + dashboardJSON.panels = [ + { + id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}, + panels: [ + {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, + {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, + ] + }, + {id: 4, type: 'row', gridPos: {x: 0, y: 1, h: 1 , w: 24}}, + {id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}}, + ]; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_ids = _.flattenDeep(_.map(dashboard.panels, (panel) => { + let ids = []; + if (panel.panels && panel.panels.length) { + ids = _.map(panel.panels, 'id'); + } + ids.push(panel.id); + return ids; + })); + expect(panel_ids.length).toEqual(_.uniq(panel_ids).length); }); - // - // it('should set scopedVars on panels', function() { - // expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}}) - // }); - // - // it.skip('should repeat row and panels below two times', function() { - // expect(dashboard.panels).toMatchObject([ - // // first (original row) - // {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}}, - // {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, - // {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, - // // repeated row - // {id: 1, type: 'row', repeatPanelId: 1, gridPos: {x: 0, y: 0, h: 1 , w: 24}}, - // {id: 2, type: 'graph', repeatPanelId: 1, gridPos: {x: 0, y: 1, h: 1 , w: 6}}, - // {id: 3, type: 'graph', repeatPanelId: 1, gridPos: {x: 6, y: 1, h: 1 , w: 6}}, - // // row below dont touch - // {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, - // {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, - // ]); - // }); }); diff --git a/public/app/features/dashboard/timepicker/dropdown.html b/public/app/features/dashboard/timepicker/dropdown.html index f8077badec5..8b137891791 100644 --- a/public/app/features/dashboard/timepicker/dropdown.html +++ b/public/app/features/dashboard/timepicker/dropdown.html @@ -1,72 +1 @@ -
    -
    -
    -
    - -
    - Time Picker -
    -
    - -
    -
    -

    Custom range

    - - -
    -
    - -
    -
    - -
    -
    - -
    - -
    - - - -
    -
    - -
    -
    - -
    -
    - -
    - -
    - - -
    -
    - -
    -
    - -
    -
    - -
    - -
    -

    Quick ranges

    -
      -
    • - -
    • -
    -
    - -
    -
    -
    diff --git a/public/app/features/dashboard/timepicker/timepicker.html b/public/app/features/dashboard/timepicker/timepicker.html index 7c57b76b957..0ffe33ed31a 100644 --- a/public/app/features/dashboard/timepicker/timepicker.html +++ b/public/app/features/dashboard/timepicker/timepicker.html @@ -24,3 +24,62 @@
    + +
    +
    +

    Time range

    + + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    +
    + +
    +
    + +
    +
    +
    + +
    +

    Quick ranges

    +
      +
    • + +
    • +
    +
    +
    + diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index 6903c090371..3f909bdefb3 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -24,9 +24,10 @@ export class TimePickerCtrl { isUtc: boolean; firstDayOfWeek: number; closeDropdown: any; + isOpen: boolean; /** @ngInject */ - constructor(private $scope, private $rootScope, private timeSrv, private popoverSrv, private $element) { + constructor(private $scope, private $rootScope, private timeSrv) { this.$scope.ctrl = this; $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope); @@ -95,6 +96,11 @@ export class TimePickerCtrl { } openDropdown() { + if (this.isOpen) { + this.isOpen = false; + return; + } + this.onRefresh(); this.editTimeRaw = this.timeRaw; this.timeOptions = rangeUtil.getRelativeTimesList(this.panel, this.rangeString); @@ -106,17 +112,7 @@ export class TimePickerCtrl { }; this.refresh.options.unshift({text: 'off'}); - - this.closeDropdown = this.popoverSrv.show({ - element: this.$element[0], - position: 'bottom center', - template: '', - openOn: 'click', - classNames: 'drop-popover drop-popover--form', - model: { - ctrl: this - }, - }); + this.isOpen = true; } applyCustom() { @@ -125,7 +121,7 @@ export class TimePickerCtrl { } this.timeSrv.setTime(this.editTimeRaw); - this.closeDropdown(); + this.isOpen = false; } absoluteFromChanged() { @@ -148,7 +144,7 @@ export class TimePickerCtrl { } this.timeSrv.setTime(range); - this.closeDropdown(); + this.isOpen = false; } } @@ -179,20 +175,8 @@ export function timePickerDirective() { }; } -export function timePickerDropdown() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/timepicker/dropdown.html', - scope: { - ctrl: "=" - } - }; -} - - angular.module('grafana.directives').directive('gfTimePickerSettings', settingsDirective); angular.module('grafana.directives').directive('gfTimePicker', timePickerDirective); -angular.module('grafana.directives').directive('gfTimePickerDropdown', timePickerDropdown); import {inputDateDirective} from './input_date'; angular.module("grafana.directives").directive('inputDatetime', inputDateDirective); diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 45a1f1b4a0c..61cca0d4216 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` -
    - - -
    -
    diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 0577c6fbfcc..323cd4e6f6b 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -20,7 +20,7 @@ var panelTemplate = `
    - +
    diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index 846202bc61d..5429a992a71 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -1,5 +1,6 @@ /// +import $ from 'jquery'; import {coreModule} from 'app/core/core'; var template = ` @@ -83,6 +84,7 @@ function panelHeader($compile) { restrict: 'E', template: template, link: function(scope, elem, attrs) { + console.log(elem.html()); let menuElem = elem.find('.panel-menu'); let menuScope; @@ -106,13 +108,37 @@ function panelHeader($compile) { } }); + elem.find('.panel-menu-toggle').click(() => { + togglePanelStackPosition(); + }); + function togglePanelMenu(e) { if (!isDragged) { e.stopPropagation(); + togglePanelStackPosition(); elem.find('[data-toggle=dropdown]').dropdown('toggle'); } } + /** + * Hack for adding special class 'dropdown-menu-open' to the panel. + * This class sets z-index for panel and prevents menu overlapping. + */ + function togglePanelStackPosition() { + const menuOpenClass = 'dropdown-menu-open'; + const panelGridClass = '.react-grid-item.panel'; + + let panelElem = elem.find('[data-toggle=dropdown]').parentsUntil('.panel').parent(); + let menuElem = elem.find('[data-toggle=dropdown]').parent(); + panelElem = panelElem && panelElem.length ? panelElem[0] : undefined; + if (panelElem) { + panelElem = $(panelElem); + $(panelGridClass).removeClass(menuOpenClass); + let state = !menuElem.hasClass('open'); + panelElem.toggleClass(menuOpenClass, state); + } + } + let mouseX, mouseY; elem.mousedown((e) => { mouseX = e.pageX; diff --git a/public/app/features/plugins/ds_list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts index 5feadb68fe3..ce9047166ef 100644 --- a/public/app/features/plugins/ds_list_ctrl.ts +++ b/public/app/features/plugins/ds_list_ctrl.ts @@ -1,6 +1,7 @@ /// import coreModule from '../../core/core_module'; +import {appEvents} from 'app/core/core'; export class DataSourcesCtrl { datasources: any; @@ -11,13 +12,24 @@ export class DataSourcesCtrl { private $scope, private backendSrv, private datasourceSrv, + private $location, private navModelSrv) { this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0); - + this.navigateToUrl = this.navigateToUrl.bind(this); backendSrv.get('/api/datasources').then(result => { this.datasources = result; }); + + appEvents.on('location-change', payload => { + this.navigateToUrl(payload.href); + }); + } + + navigateToUrl(url) { + // debugger; + this.$location.path(url); + this.$location.replace(); } removeDataSourceConfirmed(ds) { diff --git a/public/app/features/plugins/partials/ds_list.html b/public/app/features/plugins/partials/ds_list.html index 0669beb4129..b645096960c 100644 --- a/public/app/features/plugins/partials/ds_list.html +++ b/public/app/features/plugins/partials/ds_list.html @@ -1,46 +1,57 @@
    - +
    - No data sources defined +
    diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 210275d2200..1d41d4f2036 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -2,9 +2,7 @@
    - +
    diff --git a/public/app/partials/error.html b/public/app/partials/error.html index 0750e860dd5..f489251d040 100644 --- a/public/app/partials/error.html +++ b/public/app/partials/error.html @@ -1,55 +1,47 @@ - - -
    - - -
    -
    -
    -
    -
    -
    -

    100%

    -

    80%

    -

    60%

    -

    40%

    -

    20%

    -

    0%

    -
    -
    - -
    -

    Then

    -

    Now

    -
    -
    -
    -
    -
    -
    -

    current

    -
    -
    - -
    -
    -

    Chances you are on the page you are looking for.

    -

    0%

    -
    -
    -

    Sorry for the inconvenience

    -

    Please go back to your home dashboard and try again.

    -

    If the error persists, seek help on the community site.

    -
    -
    -
    -
    - -
    -
    + +
    +
    +
    +
    +
    +

    100%

    +

    80%

    +

    60%

    +

    40%

    +

    20%

    +

    0%

    +
    +
    + +
    +

    Then

    +

    Now

    +
    +
    +
    +
    +
    +
    +

    current

    +
    +
    + +
    +
    +

    Chances you are on the page you are looking for.

    +

    0%

    +
    +
    +

    Sorry for the inconvenience

    +

    Please go back to your + home dashboard and try again.

    +

    If the error persists, seek help on the + community site.

    +
    +
    +
    +
    + +
    diff --git a/public/app/partials/reset_password.html b/public/app/partials/reset_password.html index 82024932a16..f6caeef2a66 100644 --- a/public/app/partials/reset_password.html +++ b/public/app/partials/reset_password.html @@ -58,13 +58,14 @@
    -
    -
    - -
    -
    +
    + +
    + + + diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.ts b/public/app/plugins/datasource/elasticsearch/query_builder.ts index 754c541b2a8..a872d182930 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.ts +++ b/public/app/plugins/datasource/elasticsearch/query_builder.ts @@ -172,7 +172,6 @@ export class ElasticQueryBuilder { build(target, adhocFilters?, queryString?) { // make sure query has defaults; target.metrics = target.metrics || [{ type: 'count', id: '1' }]; - target.dsType = 'elasticsearch'; target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}]; target.timeField = this.timeField; diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 1b8dcf4cf3d..56635d775fb 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -19,7 +19,6 @@ export default class InfluxQuery { this.scopedVars = scopedVars; target.policy = target.policy || 'default'; - target.dsType = 'influxdb'; target.resultFormat = target.resultFormat || 'time_series'; target.orderByTime = target.orderByTime || 'ASC'; target.tags = target.tags || []; diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index ac5ccfeb5ca..5a8d3b236c0 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -103,12 +103,21 @@ export class MysqlDatasource { format: 'table', }; + var data = { + queries: [interpolatedQuery], + }; + + if (optionalOptions && optionalOptions.range && optionalOptions.range.from) { + data['from'] = optionalOptions.range.from.valueOf().toString(); + } + if (optionalOptions && optionalOptions.range && optionalOptions.range.to) { + data['to'] = optionalOptions.range.to.valueOf().toString(); + } + return this.backendSrv.datasourceRequest({ url: '/api/tsdb/query', method: 'POST', - data: { - queries: [interpolatedQuery], - } + data: data }) .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index af3d83f50d8..82354dd4bc6 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -99,12 +99,21 @@ export class PostgresDatasource { format: 'table', }; + var data = { + queries: [interpolatedQuery], + }; + + if (optionalOptions && optionalOptions.range && optionalOptions.range.from) { + data['from'] = optionalOptions.range.from.valueOf().toString(); + } + if (optionalOptions && optionalOptions.range && optionalOptions.range.to) { + data['to'] = optionalOptions.range.to.valueOf().toString(); + } + return this.backendSrv.datasourceRequest({ url: '/api/tsdb/query', method: 'POST', - data: { - queries: [interpolatedQuery], - } + data: data }) .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 7a82a428939..46d9bf9fccd 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -67,8 +67,8 @@ export default class PrometheusMetricFindQuery { return this.datasource._request("GET", url).then(function(result) { var _labels = _.map(result.data.data, function(metric) { - return metric[label]; - }); + return metric[label] || ''; + }).filter(function(label) { return label !== ''; }); return _.uniq(_labels).map(function(metric) { return { diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 98fd01eee15..8d6e89c3406 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,63 +1,60 @@ -
    -
    - - -
    -
    +
    +
    + + +
    +
    -
    -
    - - - -
    +
    +
    + + + + + Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for + the label hostname. + +
    -
    - - - - Leave blank for auto handling based on time range and panel width - -
    +
    + + + + Leave blank for auto handling based on time range and panel width + +
    -
    - -
    - -
    -
    - -
    - -
    - -
    - - - -
    - -
    -
    -
    -
    +
    + +
    + +
    +
    +
    + +
    + +
    + + + +
    +
    +
    +
    +
    diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index cb1a024a1d9..aa48a077b50 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -13,6 +13,10 @@ "alerting": true, "annotations": true, + "queryOptions": { + "minInterval": true + }, + "info": { "author": { "name": "Grafana Project", diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index bb051d7328d..adc1cc248f8 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -76,6 +76,24 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); + it('label_values(metric, resource) result should not contain empty string', function() { + response = { + status: "success", + data: [ + {__name__: "metric", resource: "value1"}, + {__name__: "metric", resource: "value2"}, + {__name__: "metric", resource: ""} + ] + }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); + var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); + pm.process().then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + ctx.$rootScope.$apply(); + expect(results.length).to.be(2); + expect(results[0].text).to.be("value1"); + expect(results[1].text).to.be("value2"); + }); it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 70b2c4547e7..13dd25755ee 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -20,7 +20,7 @@ import {EventManager} from 'app/features/annotations/all'; import {convertValuesToHistogram, getSeriesValues} from './histogram'; /** @ngInject **/ -function graphDirective($rootScope, timeSrv, contextSrv) { +function graphDirective(timeSrv, popoverSrv, contextSrv) { return { restrict: 'A', template: '', @@ -32,8 +32,6 @@ function graphDirective($rootScope, timeSrv, contextSrv) { var data; var plot; var sortedSeries; - var legendSideLastValue = null; - var rootScope = scope.$root; var panelWidth = 0; var eventManager = new EventManager(ctrl); var thresholdManager = new ThresholdManager(ctrl); @@ -51,17 +49,28 @@ function graphDirective($rootScope, timeSrv, contextSrv) { } }); - ctrl.events.on('render', function(renderData) { + /** + * Split graph rendering into two parts. + * First, calculate series stats in buildFlotPairs() function. Then legend rendering started + * (see ctrl.events.on('render') in legend.ts). + * When legend is rendered it emits 'legend-rendering-complete' and graph rendered. + */ + ctrl.events.on('render', (renderData) => { data = renderData || data; if (!data) { return; } annotations = ctrl.annotations || []; + buildFlotPairs(data); + ctrl.events.emit('render-legend'); + }); + + ctrl.events.on('legend-rendering-complete', () => { render_panel(); }); // global events - appEvents.on('graph-hover', function(evt) { + appEvents.on('graph-hover', (evt) => { // ignore other graph hover events if shared tooltip is disabled if (!dashboard.sharedTooltipModeEnabled()) { return; @@ -75,47 +84,17 @@ function graphDirective($rootScope, timeSrv, contextSrv) { tooltip.show(evt.pos); }, scope); - appEvents.on('graph-hover-clear', function(event, info) { + appEvents.on('graph-hover-clear', (event, info) => { if (plot) { tooltip.clear(plot); } }, scope); - function getLegendHeight(panelHeight) { - if (!panel.legend.show || panel.legend.rightSide) { - return 0; - } - - if (panel.legend.alignAsTable) { - var legendSeries = _.filter(data, function(series) { - return series.hideFromLegend(panel.legend) === false; - }); - var total = 23 + (21 * legendSeries.length); - return Math.min(total, Math.floor(panelHeight/2)); - } else { - return 26; - } - } - - function setElementHeight() { - try { - var height = ctrl.height - getLegendHeight(ctrl.height); - elem.css('height', height + 'px'); - - return true; - } catch (e) { // IE throws errors sometimes - console.log(e); - return false; - } - } - function shouldAbortRender() { if (!data) { return true; } - if (!setElementHeight()) { return true; } - if (panelWidth === 0) { return true; } @@ -124,27 +103,6 @@ function graphDirective($rootScope, timeSrv, contextSrv) { } function drawHook(plot) { - // Update legend values - var yaxis = plot.getYAxes(); - for (var i = 0; i < data.length; i++) { - var series = data[i]; - var axis = yaxis[series.yaxis - 1]; - var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; - - // decimal override - if (_.isNumber(panel.decimals)) { - series.updateLegendValues(formater, panel.decimals, null); - } else { - // auto decimals - // legend and tooltip gets one more decimal precision - // than graph legend ticks - var tickDecimals = (axis.tickDecimals || -1) + 1; - series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2); - } - - if (!rootScope.$$phase) { scope.$digest(); } - } - // add left axis labels if (panel.yaxes[0].label && panel.yaxes[0].show) { $("
    ").text(panel.yaxes[0].label).appendTo(elem); @@ -155,6 +113,10 @@ function graphDirective($rootScope, timeSrv, contextSrv) { $("
    ").text(panel.yaxes[1].label).appendTo(elem); } + if (ctrl.dataWarning) { + $(`
    ${ctrl.dataWarning.title}
    `).appendTo(elem); + } + thresholdManager.draw(plot); } @@ -205,7 +167,6 @@ function graphDirective($rootScope, timeSrv, contextSrv) { // Function for rendering panel function render_panel() { panelWidth = elem.width(); - if (shouldAbortRender()) { return; } @@ -216,10 +177,99 @@ function graphDirective($rootScope, timeSrv, contextSrv) { // un-check dashes if lines are unchecked panel.dashes = panel.lines ? panel.dashes : false; - var stack = panel.stack ? true : null; - // Populate element - var options: any = { + let options: any = buildFlotOptions(panel); + prepareXAxis(options, panel); + configureYAxisOptions(data, options); + thresholdManager.addFlotOptions(options, panel); + eventManager.addFlotEvents(annotations, options); + + sortedSeries = sortSeries(data, panel); + callPlot(options, true); + } + + function buildFlotPairs(data) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); + + // if hidden remove points and disable stack + if (ctrl.hiddenSeries[series.alias]) { + series.data = []; + series.stack = false; + } + } + } + + function prepareXAxis(options, panel) { + switch (panel.xaxis.mode) { + case 'series': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + + for (let i = 0; i < data.length; i++) { + let series = data[i]; + series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; + } + + addXSeriesAxis(options); + break; + } + case 'histogram': { + let bucketSize: number; + let values = getSeriesValues(data); + + if (data.length && values.length) { + let histMin = _.min(_.map(data, s => s.stats.min)); + let histMax = _.max(_.map(data, s => s.stats.max)); + let ticks = panel.xaxis.buckets || panelWidth / 50; + bucketSize = tickStep(histMin, histMax, ticks); + let histogram = convertValuesToHistogram(values, bucketSize); + data[0].data = histogram; + options.series.bars.barWidth = bucketSize * 0.8; + } else { + bucketSize = 0; + } + + addXHistogramAxis(options, bucketSize); + break; + } + case 'table': { + options.series.bars.barWidth = 0.7; + options.series.bars.align = 'center'; + addXTableAxis(options); + break; + } + default: { + options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; + addTimeAxis(options); + break; + } + } + } + + function callPlot(options, incrementRenderCounter) { + try { + plot = $.plot(elem, sortedSeries, options); + if (ctrl.renderError) { + delete ctrl.error; + delete ctrl.inspector; + } + } catch (e) { + console.log('flotcharts error', e); + ctrl.error = e.message || "Render Error"; + ctrl.renderError = true; + ctrl.inspector = {error: e}; + } + + if (incrementRenderCounter) { + ctrl.renderingCompleted(); + } + } + + function buildFlotOptions(panel) { + const stack = panel.stack ? true : null; + let options = { hooks: { draw: [drawHook], processOffset: [processOffsetHook], @@ -276,96 +326,7 @@ function graphDirective($rootScope, timeSrv, contextSrv) { mode: 'x' } }; - - for (let i = 0; i < data.length; i++) { - let series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); - - // if hidden remove points and disable stack - if (ctrl.hiddenSeries[series.alias]) { - series.data = []; - series.stack = false; - } - } - - switch (panel.xaxis.mode) { - case 'series': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - - for (let i = 0; i < data.length; i++) { - let series = data[i]; - series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; - } - - addXSeriesAxis(options); - break; - } - case 'histogram': { - let bucketSize: number; - let values = getSeriesValues(data); - - if (data.length && values.length) { - let histMin = _.min(_.map(data, s => s.stats.min)); - let histMax = _.max(_.map(data, s => s.stats.max)); - let ticks = panel.xaxis.buckets || panelWidth / 50; - bucketSize = tickStep(histMin, histMax, ticks); - let histogram = convertValuesToHistogram(values, bucketSize); - data[0].data = histogram; - options.series.bars.barWidth = bucketSize * 0.8; - } else { - bucketSize = 0; - } - - addXHistogramAxis(options, bucketSize); - break; - } - case 'table': { - options.series.bars.barWidth = 0.7; - options.series.bars.align = 'center'; - addXTableAxis(options); - break; - } - default: { - options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5; - addTimeAxis(options); - break; - } - } - - thresholdManager.addFlotOptions(options, panel); - eventManager.addFlotEvents(annotations, options); - configureAxisOptions(data, options); - - sortedSeries = sortSeries(data, ctrl.panel); - - function callPlot(incrementRenderCounter) { - try { - plot = $.plot(elem, sortedSeries, options); - if (ctrl.renderError) { - delete ctrl.error; - delete ctrl.inspector; - } - } catch (e) { - console.log('flotcharts error', e); - ctrl.error = e.message || "Render Error"; - ctrl.renderError = true; - ctrl.inspector = {error: e}; - } - - if (incrementRenderCounter) { - ctrl.renderingCompleted(); - } - } - - if (shouldDelayDraw(panel)) { - // temp fix for legends on the side, need to render twice to get dimensions right - callPlot(false); - setTimeout(function() { callPlot(true); }, 50); - legendSideLastValue = panel.legend.rightSide; - } else { - callPlot(true); - } + return options; } function sortSeries(series, panel) { @@ -408,16 +369,6 @@ function graphDirective($rootScope, timeSrv, contextSrv) { } } - function shouldDelayDraw(panel) { - if (panel.legend.rightSide) { - return true; - } - if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) { - return true; - } - return false; - } - function addTimeAxis(options) { var ticks = panelWidth / 100; var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf(); @@ -517,7 +468,7 @@ function graphDirective($rootScope, timeSrv, contextSrv) { }; } - function configureAxisOptions(data, options) { + function configureYAxisOptions(data, options) { var defaults = { position: 'left', show: panel.yaxes[0].show, diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js deleted file mode 100644 index 265efa05748..00000000000 --- a/public/app/plugins/panel/graph/legend.js +++ /dev/null @@ -1,215 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', -], -function (angular, _, $) { - 'use strict'; - - var module = angular.module('grafana.directives'); - - module.directive('graphLegend', function(popoverSrv, $timeout) { - - return { - link: function(scope, elem) { - var $container = $('
    '); - var firstRender = true; - var ctrl = scope.ctrl; - var panel = ctrl.panel; - var data; - var seriesList; - var i; - - ctrl.events.on('render', function() { - data = ctrl.seriesList; - if (data) { - render(); - } - }); - - 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 = $($container.children('tbody')).scrollTop(); - ctrl.toggleSeries(seriesInfo, e); - $($container.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.append($container); - $container.on('click', '.graph-legend-icon', openColorSelector); - $container.on('click', '.graph-legend-alias', toggleSeries); - $container.on('click', 'th', sortLegend); - firstRender = false; - } - - seriesList = data; - - $container.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" : ""; - $container.css("min-width", width); - - $container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); - - var tableHeaderElem; - if (panel.legend.alignAsTable) { - var header = ''; - header += ''; - if (panel.legend.values) { - header += getTableHeaderHtml('min'); - header += getTableHeaderHtml('max'); - header += getTableHeaderHtml('avg'); - header += getTableHeaderHtml('current'); - header += getTableHeaderHtml('total'); - } - 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(); - } - } - - 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 = ''; + header += ''; + if (panel.legend.values) { + header += getTableHeaderHtml('min'); + header += getTableHeaderHtml('max'); + header += getTableHeaderHtml('avg'); + header += getTableHeaderHtml('current'); + header += getTableHeaderHtml('total'); + } + 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 += '
    ' + 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)); + } + return seriesElements; + } + + function renderLegendElement(tableHeaderElem) { + var seriesElements = renderSeriesLegendElements(); + + if (panel.legend.alignAsTable) { + var tbodyElem = $(''); + tbodyElem.append(tableHeaderElem); + tbodyElem.append(seriesElements); + elem.append(tbodyElem); + } else { + elem.append(seriesElements); + + if (!legendScrollbar) { + legendScrollbar = new PerfectScrollbar(elem[0]); + } else { + legendScrollbar.update(); + } + } + } + } + }; +}); diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 17445dd3002..7d310b078c0 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -87,6 +87,8 @@ describe('grafanaGraph', function() { $.plot = ctx.plotSpy = sinon.spy(); ctrl.events.emit('render', ctx.data); + ctrl.events.emit('render-legend'); + ctrl.events.emit('legend-rendering-complete'); ctx.plotData = ctx.plotSpy.getCall(0).args[1]; ctx.plotOptions = ctx.plotSpy.getCall(0).args[2]; })); diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts index 6ded97ce33a..0b9eb8227df 100644 --- a/public/app/plugins/panel/graph/template.ts +++ b/public/app/plugins/panel/graph/template.ts @@ -1,22 +1,10 @@ var template = ` -
    -
    - -
    - {{ctrl.dataWarning.title}} -
    - -
    -
    - +
    +
    -
    -
    - -
    +
    +
    `; export default template; - - diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 6d631171966..2cbf193bdc6 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -87,6 +87,7 @@ @import "components/dashboard_list"; @import "components/page_header"; @import "components/dashboard_settings"; +@import "components/empty_list_cta"; // PAGES diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index 72f4332a51e..fa119047f44 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -18,6 +18,14 @@ // Media queries // --------------------- +@include media-breakpoint-down(xs) { + input[type='text'], + input[type='number'], + textarea { + font-size: 16px; + } +} + @include media-breakpoint-up(sm) { .navbar-page-btn { max-width: 250px; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss new file mode 100644 index 00000000000..c0d1757dae7 --- /dev/null +++ b/public/sass/_variables.dark.scss @@ -0,0 +1,361 @@ +// Global values +// -------------------------------------------------- + +$theme-name: dark; + +// Grays +// ------------------------- +$black: #000; + +// ------------------------- +$black: #000; +$dark-1: #141414; +$dark-2: #1f1f20; +$dark-3: #262628; +$dark-4: #333333; +$dark-5: #444444; +$gray-1: #555555; +$gray-2: #8e8e8e; +$gray-3: #b3b3b3; +$gray-4: #D8D9DA; +$gray-5: #ECECEC; +$gray-6: #f4f5f8; +$gray-7: #fbfbfb; +$gray-blue: #292a2d; + +$white: #fff; + +// Accent colors +// ------------------------- +$blue: #33B5E5; +$blue-dark: #005f81; +$green: #299c46; +$red: #d44a3a; +$yellow: #ECBB13; +$pink: #FF4444; +$purple: #9933CC; +$variable: #32D1DF; +$orange: #eb7b18; + +$brand-primary: $orange; +$brand-success: $green; +$brand-warning: $brand-primary; +$brand-danger: $red; + +// Status colors +// ------------------------- +$online: #10a345; +$warn: #F79520; +$critical: #ed2e18; + +// Scaffolding +// ------------------------- +$body-bg: rgb(23,24,25); +$page-bg: rgb(22, 23, 25); + +$body-color: $gray-4; +$text-color: $gray-4; +$text-color-strong: $white; +$text-color-weak: $gray-2; +$text-color-faint: $dark-5; +$text-color-emphasis: $gray-5; + +$text-shadow-strong: 1px 1px 4px $black; +$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); + +// gradients +$brand-gradient: linear-gradient(to right, rgba(255,213,0,0.7) 0%, rgba(255,68,0,0.7) 99%, rgba(255,68,0,0.7) 100%); +$page-gradient: linear-gradient(180deg, #222426 10px, rgb(22, 23, 25) 100px); + +// Links +// ------------------------- +$link-color: darken($white, 11%); +$link-color-disabled: darken($link-color, 30%); +$link-hover-color: $white; +$external-link-color: $blue; + +// Typography +// ------------------------- +$headings-color: darken($white,11%); +$abbr-border-color: $gray-3 !default; +$text-muted: $text-color-weak; + +$blockquote-small-color: $gray-3 !default; +$blockquote-border-color: $gray-4 !default; + +$hr-border-color: rgba(0,0,0,.1) !default; + +// Components +$component-active-color: #fff !default; +$component-active-bg: $brand-primary !default; + +// Panel +// ------------------------- +$panel-bg: #212124; +$panel-border-color: $dark-1; +$panel-border: solid 1px $panel-border-color; +$panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191919 10px, #222 20px); +$panel-header-hover-bg: $dark-4; +$panel-header-menu-hover-bg: $dark-5; + +// page header +$page-header-bg: linear-gradient(90deg, #292a2d, black); +$page-header-shadow: inset 0px -4px 14px $dark-2; +$page-header-border-color: $dark-4; + +$divider-border-color: #555; + +// Graphite Target Editor +$tight-form-bg: $dark-3; + +$tight-form-func-bg: #333334; +$tight-form-func-highlight-bg: #444445; + +$modal-backdrop-bg: #353c42 ; +$code-tag-bg: $gray-1; +$code-tag-border: lighten($code-tag-bg, 2%); + +// cards +$card-background: linear-gradient(135deg, #2f2f32, #262628); +$card-background-hover: linear-gradient(135deg, #343436, #262628); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3); + +// Lists +$list-item-bg: $card-background; +$list-item-hover-bg: lighten($gray-blue, 2%); +$list-item-link-color: $text-color; +$list-item-shadow: $card-shadow; + +// Scrollbars +$scrollbarBackground: #404357; +$scrollbarBackground2: #3a3a3a; + +$scrollbarBorder: black; + +// Tables +// ------------------------- +$table-bg: transparent; // overall background-color +$table-bg-accent: $dark-3; // for striping +$table-bg-hover: $dark-4; // for hover +$table-border: $dark-3; // table and cell border + +$table-bg-odd: $dark-2; + +// Buttons +// ------------------------- + +$btn-primary-bg: #ff6600; +$btn-primary-bg-hl: #bc3e06; + +$btn-secondary-bg: $blue-dark; +$btn-secondary-bg-hl: lighten($blue-dark, 5%); + +$btn-success-bg: $green; +$btn-success-bg-hl: darken($green, 6%); + +$btn-warning-bg: $brand-warning; +$btn-warning-bg-hl: lighten($brand-warning, 8%); + +$btn-danger-bg: $red; +$btn-danger-bg-hl: darken($red, 8%); + +$btn-inverse-bg: $dark-3; +$btn-inverse-bg-hl: lighten($dark-3, 4%); +$btn-inverse-text-color: $link-color; +$btn-inverse-text-shadow: 0px 1px 0 rgba(0,0,0,.1); + +$btn-link-color: $gray-3; + +$iconContainerBackground: $black; + +$btn-divider-left: $dark-4; +$btn-divider-right: $dark-2; + +$btn-drag-image: '../img/grab_dark.svg'; + +// Forms +// ------------------------- +$input-bg: $black; +$input-bg-disabled: $dark-3; + +$input-color: $gray-4; +$input-border-color: $dark-3; +$input-box-shadow: inset 1px 0px 0.3rem 0px rgba(150, 150, 150, 0.10); +$input-border-focus: $input-border-color !default; +$input-box-shadow-focus: rgba(102,175,233,.6) !default; +$input-color-placeholder: $gray-1 !default; +$input-label-bg: $gray-blue; +$input-label-border-color: $gray-blue; +$input-invalid-border-color: lighten($red, 5%); + +// Search +$search-shadow: 0 0 35px 0 $body-bg; +$search-filter-box-bg: $gray-blue; + +// Dropdowns +// ------------------------- +$dropdownBackground: $dark-3; +$dropdownBorder: rgba(0,0,0,.2); +$dropdownDividerTop: transparent; +$dropdownDividerBottom: #444; +$dropdownDivider: $dropdownDividerBottom; +$dropdownTitle: $link-color-disabled; + +$dropdownLinkColor: $text-color; +$dropdownLinkColorHover: $white; +$dropdownLinkColorActive: $white; + +$dropdownLinkBackgroundActive: $dark-4; +$dropdownLinkBackgroundHover: $dark-4; + +$dropdown-link-color: $gray-3; + +// COMPONENT VARIABLES +// -------------------------------------------------- + +// ------------------------- +$placeholderText: darken($text-color, 25%); + + +// Horizontal forms & lists +// ------------------------- +$horizontalComponentOffset: 180px; + + +// Wells +// ------------------------- +$wellBackground: #131517; + +$navbarHeight: 52px; +$navbarBackgroundHighlight: $dark-3; +$navbarBackground: $panel-bg; +$navbarBorder: 1px solid $body-bg; +$navbarShadow: 0 0 20px black; + +$navbarText: $gray-4; +$navbarLinkColor: $gray-4; +$navbarLinkColorHover: $white; +$navbarLinkColorActive: $navbarLinkColorHover; +$navbarLinkBackgroundHover: transparent; +$navbarLinkBackgroundActive: $navbarBackground; +$navbarBrandColor: $link-color; +$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg; + +$navbarButtonBackground: $navbarBackground; +$navbarButtonBackgroundHighlight: $body-bg; + +$navbar-button-border: #151515; + +// Sidemenu +// ------------------------- +$side-menu-bg: $black; +$side-menu-item-hover-bg: $dark-2; +$side-menu-shadow: 0 0 20px black; +$side-menu-link-color: $link-color; +$breadcrumb-hover-hl: #111; + +// Menu dropdowns +// ------------------------- +$menu-dropdown-bg: $body-bg; +$menu-dropdown-hover-bg: $dark-2; +$menu-dropdown-border-color: $dark-3; +$menu-dropdown-shadow: 5px 5px 20px -5px $black; + +// Breadcrumb +// ------------------------- +$page-nav-bg: $black; +$page-nav-shadow: 5px 5px 20px -5px $black; +$page-nav-breadcrumb-color: $gray-3; + +// Tabs +// ------------------------- +$tab-border-color: $dark-4; + +// Pagination +// ------------------------- +$paginationBackground: $body-bg; +$paginationBorder: transparent; +$paginationActiveBackground: $blue; + +// Form states and alerts +// ------------------------- +$warning-text-color: $warn; +$error-text-color: #E84D4D; +$success-text-color: #12D95A; +$info-text-color: $blue-dark; + +$alert-error-bg: linear-gradient(90deg, #d44939, #e0603d); +$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); +$alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); +$alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); + +// popover +$popover-bg: $page-bg; +$popover-color: $text-color; +$popover-border-color: $dark-4; +$popover-shadow: 0 0 20px black; + +$popover-help-bg: $btn-secondary-bg; +$popover-help-color: $text-color; + +$popover-error-bg: $btn-danger-bg; + +// Tooltips and popovers +// ------------------------- +$tooltipColor: $popover-help-color; +$tooltipBackground: $popover-help-bg; +$tooltipArrowWidth: 5px; +$tooltipArrowColor: $tooltipBackground; +$tooltipLinkColor: $link-color; +$graph-tooltip-bg: $dark-1; + +// images +$checkboxImageUrl: '../img/checkbox.png'; + +// info box +$info-box-background: linear-gradient(100deg, #1a4552, #00374a); + +// footer +$footer-link-color: $gray-2; +$footer-link-hover: $gray-4; + +// collapse box +$collapse-box-body-border: $dark-5; +$collapse-box-body-error-border: $red; + +// json-explorer +$json-explorer-default-color: $text-color; +$json-explorer-string-color: #23d662; +$json-explorer-number-color: $variable; +$json-explorer-boolean-color: $variable; +$json-explorer-null-color: #EEC97D; +$json-explorer-undefined-color: rgb(239, 143, 190); +$json-explorer-function-color: #FD48CB; +$json-explorer-rotate-time: 100ms; +$json-explorer-toggler-opacity: 0.6; +$json-explorer-toggler-color: #45376F; +$json-explorer-bracket-color: #9494FF; +$json-explorer-key-color: #23A0DB; +$json-explorer-url-color: #027BFF; + +// Changelog and diff +// ------------------------- +$diff-label-bg: $dark-2; +$diff-label-fg: $white; + +$diff-group-bg: $dark-4; +$diff-arrow-color: $white; + +$diff-json-bg: $dark-4; +$diff-json-fg: $gray-5; + +$diff-json-added: #457740; +$diff-json-deleted: #a04338; + +$diff-json-old: #a04338; +$diff-json-new: #457740; + +$diff-json-changed-fg: $gray-5; +$diff-json-changed-num: $text-color; + +$diff-json-icon: $gray-7; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss new file mode 100644 index 00000000000..f6512aed3c4 --- /dev/null +++ b/public/sass/_variables.light.scss @@ -0,0 +1,379 @@ +// Cosmo 2.3.2 +// Variables +// -------------------------------------------------- + + +// Global values +// -------------------------------------------------- + +$theme-name: light; + +// Grays +// ------------------------- +$black: #000; + +// ------------------------- +$black: #000; +$dark-1: #13161d; +$dark-2: #1e2028; +$dark-3: #303133; +$dark-4: #35373f; +$dark-5: #41444b; +$gray-1: #52545c; +$gray-2: #767980; +$gray-3: #acb6bf; +$gray-4: #c7d0d9; +$gray-5: #dde4ed; +$gray-6: #e9edf2; +$gray-7: #f7f8fa; + +$white: #fff; + +// Accent colors +// ------------------------- +$blue: #1ca4d6; +$blue-dark: #3CAAD6; +$green: #3aa655; +$red: #d44939; +$yellow: #FF851B; +$orange: #Ff7941; +$pink: #E671B8; +$purple: #9954BB; +$variable: $blue; + +$brand-primary: $orange; +$brand-success: $green; +$brand-warning: $orange; +$brand-danger: $red; + +// Status colors +// ------------------------- +$online: #01A64F; +$warn: #F79520; +$critical: #EC2128; + +// Scaffolding +// ------------------------- + +$body-bg: $gray-7; +$page-bg: $gray-7; +$body-color: $gray-1; +$text-color: $dark-4; +$text-color-strong: $white; +$text-color-weak: $gray-2; +$text-color-faint: $gray-4; +$text-color-emphasis: $dark-5; + +$text-shadow-strong: none; +$text-shadow-faint: none; +$textShadow: none; + +// gradients +$brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%); +$page-gradient: linear-gradient(-60deg, transparent 70%, $gray-7 98%); + +// Links +// ------------------------- +$link-color: $gray-1; +$link-color-disabled: lighten($link-color, 30%); +$link-hover-color: darken($link-color, 20%); +$external-link-color: $blue; + +// Typography +// ------------------------- +$headings-color: $text-color; +$abbr-border-color: $gray-2 !default; +$text-muted: $text-color-weak; + +$blockquote-small-color: $gray-2 !default; +$blockquote-border-color: $gray-3 !default; + +$hr-border-color: $dark-3 !default; + +// Components +$component-active-color: $white !default; +$component-active-bg: $brand-primary !default; + +// Panel +// ------------------------- + +$panel-bg: $white; +$panel-border-color: $gray-5; +$panel-border: solid 1px $panel-border-color; +$panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px); +$panel-header-hover-bg: $gray-6; +$panel-header-menu-hover-bg: $gray-4; + +// Page header +$page-header-bg: linear-gradient(90deg, $white, $gray-7); +$page-header-shadow: inset 0px -3px 10px $gray-6; +$page-header-border-color: $gray-4; + +$divider-border-color: $gray-2; + +// Graphite Target Editor +$tight-form-bg: #eaebee; + +$tight-form-func-bg: $gray-5; +$tight-form-func-highlight-bg: $gray-6; + +$modal-backdrop-bg: $body-bg; +$code-tag-bg: $gray-6; +$code-tag-border: darken($code-tag-bg, 3%); + +// cards +$card-background: linear-gradient(135deg, $gray-6, $gray-5); +$card-background-hover: linear-gradient(135deg, $gray-5, $gray-6); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); + +// Lists +$list-item-bg: linear-gradient(135deg, $gray-5, $gray-6);//$card-background; +$list-item-hover-bg: darken($gray-5, 5%); +$list-item-link-color: $text-color; +$list-item-shadow: $card-shadow; + +// Tables +// ------------------------- +$table-bg: transparent; // overall background-color +$table-bg-accent: $gray-5; // for striping +$table-bg-hover: $gray-5; // for hover +$table-bg-active: $table-bg-hover !default; +$table-border: $gray-3; // table and cell border + +$table-bg-odd: $gray-5; + +// Scrollbars +$scrollbarBackground: $gray-5; +$scrollbarBackground2: $gray-5; +$scrollbarBorder: $gray-4; + +// Buttons +// ------------------------- +$btn-primary-bg: $brand-primary; +$btn-primary-bg-hl: lighten($brand-primary, 8%); + +$btn-secondary-bg: $blue-dark; +$btn-secondary-bg-hl: lighten($blue-dark, 4%); + +$btn-success-bg: lighten($green, 3%); +$btn-success-bg-hl: darken($green, 3%); + +$btn-warning-bg: lighten($orange, 3%); +$btn-warning-bg-hl: darken($orange, 3%); + +$btn-danger-bg: lighten($red, 3%); +$btn-danger-bg-hl: darken($red, 3%); + +$btn-inverse-bg: $gray-6; +$btn-inverse-bg-hl: darken($gray-6, 5%); +$btn-inverse-text-color: $gray-1; +$btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, .4); + +$btn-link-color: $gray-1; + +$btn-divider-left: $gray-4; +$btn-divider-right: $gray-7; +$btn-drag-image: '../img/grab_light.svg'; + +$iconContainerBackground: $white; + +// Forms +// ------------------------- +$input-bg: $white; +$input-bg-disabled: $gray-5; + +$input-color: $dark-3; +$input-border-color: $gray-5; +$input-box-shadow: none; +$input-border-focus: $blue !default; +$input-box-shadow-focus: $blue !default; +$input-color-placeholder: $gray-4 !default; +$input-label-bg: $gray-5; +$input-label-border-color: $gray-5; +$input-invalid-border-color: lighten($red, 5%); + +// Sidemenu +// ------------------------- +$side-menu-bg: $dark-2; +$side-menu-item-hover-bg: $gray-1; +$side-menu-shadow: 5px 0px 10px -5px $gray-1; +$side-menu-link-color: $gray-6; + +// Menu dropdowns +// ------------------------- +$menu-dropdown-bg: $gray-7; +$menu-dropdown-hover-bg: $gray-6; +$menu-dropdown-border-color: $gray-4; +$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1; + +// Breadcrumb +// ------------------------- +$page-nav-bg: $gray-5; +$page-nav-shadow: 5px 5px 20px -5px $gray-4; +$page-nav-breadcrumb-color: $black; +$breadcrumb-hover-hl: #d9dadd; + +// Tabs +// ------------------------- +$tab-border-color: $gray-5; + +// search +$search-shadow: 0 5px 30px 0 $gray-4; +$search-filter-box-bg: $gray-7; + +// Dropdowns +// ------------------------- +$dropdownBackground: $white; +$dropdownBorder: $gray-4; +$dropdownDividerTop: $gray-6; +$dropdownDividerBottom: $white; +$dropdownDivider: $dropdownDividerTop; +$dropdownTitle: $gray-3; + +$dropdownLinkColor: $dark-3; +$dropdownLinkColorHover: $link-color; +$dropdownLinkColorActive: $link-color; + +$dropdownLinkBackgroundActive: $gray-6; +$dropdownLinkBackgroundHover: $gray-6; + +// COMPONENT VARIABLES +// -------------------------------------------------- + + +// Input placeholder text color +// ------------------------- +$placeholderText: $gray-2; + + +// Hr border color +// ------------------------- +$hrBorder: $gray-3; + + +// Horizontal forms & lists +// ------------------------- +$horizontalComponentOffset: 180px; + + +// Wells +// ------------------------- +$wellBackground: $gray-3; + + +// Navbar +// ------------------------- + +$navbarHeight: 52px; +$navbarBackgroundHighlight: $white; +$navbarBackground: $white; +$navbarBorder: 1px solid $gray-4; +$navbarShadow: 0 0 3px #c1c1c1; + +$navbarText: #444; +$navbarLinkColor: #444; +$navbarLinkColorHover: #000; +$navbarLinkColorActive: #333; +$navbarLinkBackgroundHover: transparent; +$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%); +$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%); + +$navbarBrandColor: $navbarLinkColor; + +$navbarButtonBackground: lighten($navbarBackground, 3%); +$navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%); + +$navbar-button-border: $gray-4; + + +// Pagination +// ------------------------- +$paginationBackground: $gray-2; +$paginationBorder: transparent; +$paginationActiveBackground: $blue; + + +// Form states and alerts +// ------------------------- +$warning-text-color: lighten($orange, 10%); +$error-text-color: lighten($red, 10%); +$success-text-color: lighten($green, 10%); +$info-text-color: $blue; + +$alert-error-bg: linear-gradient(90deg, #d44939, #e04d3d); +$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); +$alert-warning-bg: linear-gradient(90deg, #d44939, #e04d3d); +$alert-info-bg: $blue-dark; + +// popover +$popover-bg: $page-bg; +$popover-color: $text-color; +$popover-border-color: $gray-5; +$popover-shadow: 0 0 20px $white; + +$popover-help-bg: $blue-dark; +$popover-help-color: $gray-6; +$popover-error-bg: $btn-danger-bg; + +// Tooltips and popovers +// ------------------------- +$tooltipColor: $popover-help-color; +$tooltipBackground: $popover-help-bg; +$tooltipArrowWidth: 5px; +$tooltipArrowColor: $tooltipBackground; +$tooltipLinkColor: lighten($popover-help-color, 5%); +$graph-tooltip-bg: $gray-5; + +// images +$checkboxImageUrl: '../img/checkbox_white.png'; + +// info box +// $info-box-background: linear-gradient(135deg, #f1fbff, #d7ebff); +$info-box-background: linear-gradient(135deg, $blue, $blue-dark); + +// footer +$footer-link-color: $gray-3; +$footer-link-hover: $dark-5; + +// collapse box +$collapse-box-body-border: $gray-4; +$collapse-box-body-error-border: $red; + +// json explorer +$json-explorer-default-color: black; +$json-explorer-string-color: green; +$json-explorer-number-color: blue; +$json-explorer-boolean-color: red; +$json-explorer-null-color: #855A00; +$json-explorer-undefined-color: rgb(202, 11, 105); +$json-explorer-function-color: #FF20ED; +$json-explorer-rotate-time: 100ms; +$json-explorer-toggler-opacity: 0.6; +$json-explorer-toggler-color: #45376F; +$json-explorer-bracket-color: blue; +$json-explorer-key-color: #00008B; +$json-explorer-url-color: blue; + +// Changelog and diff +// ------------------------- +$diff-label-bg: $gray-5; +$diff-label-fg: $gray-2; + +$diff-switch-bg: $gray-7; +$diff-switch-disabled: $gray-5; + +$diff-arrow-color: $dark-3; +$diff-group-bg: $gray-7; + +$diff-json-bg: $gray-5; +$diff-json-fg: $gray-2; + +$diff-json-added: lighten(desaturate($green, 30%), 10%); +$diff-json-deleted: desaturate($red, 35%); + +$diff-json-old: #5a372a; +$diff-json-new: #664e33; + +$diff-json-changed-fg: $gray-6; +$diff-json-changed-num: $gray-4; +$diff-json-icon: $gray-4; diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 49c5addc72d..9a1e42995b3 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -75,7 +75,7 @@ $container-max-widths: ( $grid-columns: 12 !default; $grid-gutter-width: 30px !default; -$enable-flex: false; +$enable-flex: true; // Typography // ------------------------- @@ -218,10 +218,13 @@ $btn-font-weight: 500 !default; $btn-padding-x-sm: .5rem !default; $btn-padding-y-sm: .25rem !default; -$btn-padding-x-lg: 1.5rem !default; -$btn-padding-y-lg: .75rem !default; +$btn-padding-x-lg: 21px !default; +$btn-padding-y-lg: 11px !default; -$btn-border-radius: 3px; +$btn-padding-x-xl: 21px !default; +$btn-padding-y-xl: 11px !default; + +$btn-border-radius: 2px; // sidemenu $side-menu-width: 60px; @@ -232,5 +235,5 @@ $dashboard-padding: $panel-margin * 2; $panel-padding: 0px 10px 5px 10px; // tabs -$tabs-padding: 10px 15px 10px; +$tabs-padding: 10px 15px 9px; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index 538733be2ef..e7f76de5f69 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -15,6 +15,10 @@ background-image: url('../img/icons_#{$theme-name}_theme/icon_alert.svg'); } +.gicon-alert-alt { + background-image: url('../img/icons_#{$theme-name}_theme/icon_alert_alt.svg'); +} + .gicon-datasources { background-image: url('../img/icons_#{$theme-name}_theme/icon_data_sources.svg'); } @@ -58,3 +62,14 @@ .gicon-zoom-out { background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg'); } + +.sidemenu { + .gicon-dashboard { + background-image: url('../img/icons_dark_theme/icon_dashboard.svg'); + } + .gicon-alert { + background-image: url('../img/icons_dark_theme/icon_alert.svg'); + } +} + + diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index fa6539a7175..1d2fed48830 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -48,6 +48,8 @@ a.text-success:hover, a.text-success:focus { color: darken($success-text-color, 10%); } a { cursor: pointer; } +.text-link { text-decoration: underline; } + a:focus { outline:0 none !important; } diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index cea04c28cd7..a53d35fc1da 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -51,10 +51,21 @@ // Button Sizes // -------------------------------------------------- +// XLarge +.btn-xlarge { + @include button-size($btn-padding-y-xl, $btn-padding-x-xl, $font-size-lg, $btn-border-radius); + font-weight: normal; + padding-bottom: $btn-padding-y-xl - 3; + .gicon { + font-size: 31px; + margin-right: 1rem; + } +} // Large .btn-large { @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius); + font-weight: normal; } .btn-small { @@ -95,7 +106,7 @@ } // Inverse appears as dark gray .btn-inverse { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); //background: $card-background; box-shadow: $card-shadow; //border: 1px solid $tight-form-func-highlight-bg; diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index c5979842206..9257db6c7dd 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -200,9 +200,8 @@ } .card-item { - border-bottom: .2rem solid $page-bg; - border-radius: 0; - box-shadow: none; + border-bottom: 3px solid $page-bg; + border-radius: 2px; } .card-item-header { diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index a6dd09bcd5a..656efe4c880 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -8,7 +8,7 @@ } .react-grid-item { - display: none; + display: none !important; transition-property: none !important; } @@ -47,3 +47,18 @@ } } +// Hack for preventing panel menu overlapping. +.react-grid-item.resizing.panel, +.react-grid-item.panel.dropdown-menu-open, +.react-grid-item.react-draggable-dragging.panel { + z-index: $zindex-dropdown; +} + +// Disable animation on initial rendering and enable it when component has been mounted. +.react-grid-item.cssTransforms.panel { + transition-property: none; +} + +.animated .react-grid-item.cssTransforms.panel { + transition-property: transform; +} diff --git a/public/sass/components/_dashboard_list.scss b/public/sass/components/_dashboard_list.scss index 6863dfa785b..e1574f46da6 100644 --- a/public/sass/components/_dashboard_list.scss +++ b/public/sass/components/_dashboard_list.scss @@ -1,19 +1,25 @@ .dashboard-list { - height: 75%; - .search-results-container { - padding-left: 0; + padding: 5px 0 0 0; } } .search-results-filter-row { + height: 35px; display: flex; justify-content: space-between; + + .gf-form-button-row { + padding-top: 0; + + button:last-child { + margin-right: 0; + } + } } .search-results-filter-row__filters { display: flex; - width: 300px; } .search-results-filter-row__filters-item { diff --git a/public/sass/components/_empty_list_cta.scss b/public/sass/components/_empty_list_cta.scss new file mode 100644 index 00000000000..a8c235d83db --- /dev/null +++ b/public/sass/components/_empty_list_cta.scss @@ -0,0 +1,21 @@ +.empty-list-cta { + background-color: $search-filter-box-bg; + text-align: center; +} + +.empty-list-cta__title { + padding-bottom: 30px; + font-style: italic; +} + +.empty-list-cta__button { + margin-bottom: 50px; +} + +.empty-list-cta__pro-tip { + padding-bottom: 20px; +} + +.empty-list-cta__pro-tip-link { + margin-left: 5px; +} \ No newline at end of file diff --git a/public/sass/components/_filter-table.scss b/public/sass/components/_filter-table.scss index 5b6f8695a31..074286fad80 100644 --- a/public/sass/components/_filter-table.scss +++ b/public/sass/components/_filter-table.scss @@ -17,7 +17,7 @@ tbody { tr:nth-child(odd) { - background: $dark-2; + background: $table-bg-odd; } } @@ -34,7 +34,6 @@ padding: $table-cell-padding; line-height: 30px; height: 30px; - border-bottom: 1px solid black; white-space: nowrap; &.filter-table__switch-cell { diff --git a/public/sass/components/_modals.scss b/public/sass/components/_modals.scss new file mode 100644 index 00000000000..81009d20f3c --- /dev/null +++ b/public/sass/components/_modals.scss @@ -0,0 +1,172 @@ +// +// Modals +// -------------------------------------------------- + +// Background +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zindex-modal-backdrop; + background-color: $modal-backdrop-bg; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + @include opacity(70); +} + +// Base modal +.modal { + position: fixed; + z-index: $zindex-modal; + width: 100%; + background: $page-bg; + @include box-shadow(0 3px 7px rgba(0,0,0,0.3)); + @include background-clip(padding-box); + outline: none; + + max-width: 750px; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + top: 10%; +} + +.modal-header { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; + @include clearfix(); +} + +.modal-header-title { + font-size: $font-size-h3; + float: left; + padding-top: $spacer * 0.75; + margin: 0 $spacer*3 0 $spacer*1.5; +} + +.modal-header-close { + float: right; + padding: 0.75rem $spacer; +} + +// Body (where all modal content resides) +.modal-body { + position: relative; +} + +.modal-content { + padding: $spacer*2; +} + +// Remove bottom margin if need be +.modal-form { + margin-bottom: 0; +} + +// Footer (for actions) +.modal-footer { + padding: 14px 15px 15px; + border-top: 1px solid $panel-bg; + background-color: $panel-bg; + text-align: right; // right align buttons + @include clearfix(); // clear it in case folks use .pull-* classes on buttons +} + +.modal--narrow { + max-width: 500px; +} + +.confirm-modal { + max-width: 500px; + + .confirm-modal-icon { + padding-top: 41px; + font-size: 280%; + color: $green; + padding-bottom: 20px; + } + + .confirm-modal-text { + font-size: $font-size-h4; + color: $link-color; + margin-bottom: $spacer*2; + padding-top: $spacer; + } + + .confirm-modal-text2 { + font-size: $font-size-root; + padding-top: $spacer; + } + + .confirm-modal-buttons { + margin-bottom: $spacer; + button { + margin-right: $spacer/2; + } + } + + .modal-content-confirm-text { + margin-bottom: 2rem; + span { + text-align: center; + } + } +} + +.share-modal-body { + padding: 10px 0; + + .tight-form { + text-align: left; + } + + .share-modal-options { + margin: 11px 0px 33px 0px; + display: inline-block; + } + + .share-modal-big-icon { + margin-bottom: 10px; + margin-right: 2rem; + .fa, .icon-gf { + font-size: 50px; + } + } + + .share-modal-info-text { + margin-top: 5px; + strong { + color: $headings-color; + font-weight: 500; + } + } + + .share-modal-header { + display: flex; + margin: 0px 0 22px 0; + } + + .share-modal-content { + flex-grow: 1; + } + + .tight-form { + text-align: left; + } + + .share-modal-link { + max-width: 716px; + white-space: nowrap; + overflow: hidden; + display: block; + text-overflow: ellipsis; + } +} + + diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index c6416421be4..25d7c6ffa82 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -74,15 +74,15 @@ } .navbar-button { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); display: inline-block; font-weight: $btn-font-weight; padding: 8px 11px; line-height: 16px; color: $text-muted; - border: 1px solid #151515; - margin-right: 1px; + border: 1px solid $navbar-button-border; + margin-right: 3px; white-space: nowrap; .gicon { diff --git a/public/sass/components/_page_header.scss b/public/sass/components/_page_header.scss new file mode 100644 index 00000000000..dc570e8f13c --- /dev/null +++ b/public/sass/components/_page_header.scss @@ -0,0 +1,174 @@ + +.page-header-canvas { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; +} + +.page-header { + padding: 2.5rem 0 0 0; + + .btn { + float: right; + margin-left: 1rem; + + // better align icons + .fa { + position: relative; + top: 1px; + } + } +} + +.page-header__inner { + flex-grow: 1; + display: flex; + margin-bottom: 2.5rem; +} + +.page-header__title { + font-size: $font-size-h2; + margin-bottom: 1px; + padding-top: $spacer; +} + +.page-header__img { + border-radius: 50%; + position: relative; + top: 10px; + width: 50px; + height: 50px; +} + +.page-header__icon { + font-size: 50px; + width: 50px; + height: 50px; + position: relative; + + &.fa { + top: 10px; + } + + &.gicon { + top: 10px; + } + + &.icon-gf { + top: 3px; + } +} + +.page-header__logo { + margin: 0 $spacer; +} + +.page-header__sub-title { + color: $text-muted; +} + +.page-header-stamps-type { + color: $link-color-disabled; + text-transform: uppercase; +} + +.page-header__select_nav { + margin-bottom: 10px; + + @include media-breakpoint-up(lg) { + display: none; + } +} + +.page-header__tabs { + display: none; + @include media-breakpoint-up(lg) { + display: block; + } +} + +.page-breadcrumbs { + display: flex; + padding: 10px 0; + line-height: 0.5; +} + +.breadcrumb { + display: inline-block; + box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.35); + overflow: hidden; + border-radius: 5px; + counter-reset: flag; +} + +.breadcrumb-item { + @include gradientBar($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + + text-decoration: none; + outline: none; + display: block; + float: left; + font-size: 12px; + line-height: 30px; + padding: 0 7px 0 37px; + position: relative; + box-shadow: $card-shadow; + + &:first-child { + padding-left: 10px; + border-radius: 5px 0 0 5px; /*to match with the parent's radius*/ + font-size: 18px; + } + + &:first-child::before { + left: 14px; + } + + &:last-child { + border-radius: 0 5px 5px 0; /*this was to prevent glitches on hover*/ + padding-right: 20px; + } + + &.active, + &:hover { + background: #333; + background: linear-gradient(#333, #000); + } + + &.active::after, + &:hover::after { + background: #333; + background: linear-gradient(135deg, #333, #000); + } + + &::after { + content: ''; + position: absolute; + top: 0; + right: -14px; // half of square's length + + // same dimension as the line-height of .breadcrumb-item + width: 30px; + height: 30px; + + transform: scale(0.707) rotate(45deg); + // we need to prevent the arrows from getting buried under the next link + z-index: 1; + + // background same as links but the gradient will be rotated to compensate with the transform applied + background: linear-gradient(135deg, $btn-inverse-bg, $btn-inverse-bg-hl); + + // stylish arrow design using box shadow + box-shadow: 2px -2px 0 2px rgb(35, 31, 31), 3px -3px 0 2px rgba(255, 255, 255, 0.1); + + // 5px - for rounded arrows and + // 50px - to prevent hover glitches on the border created using shadows*/ + border-radius: 0 5px 0 50px; + } + + // we dont need an arrow after the last link + &:last-child::after { + content: none; + } +} + diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index be645574342..19eb90b2ea6 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -35,6 +35,7 @@ height: calc(100% - 43px); align-content: flex-start; justify-content: space-around; + position: relative; } .add-panel__item { diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index b5c0ec2c66a..c28a608052d 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -1,10 +1,31 @@ -.graph-canvas-wrapper { - position: relative; - cursor: crosshair; +.graph-panel { + display: flex; + flex-direction: column; + height: 100%; + + &--legend-right { + flex-direction: row; + + .graph-legend { + flex: 0 1 10px; + max-height: 100%; + } + + .graph-legend-series { + display: block; + padding-left: 0px; + } + + .graph-legend-table .graph-legend-series { + display: table-row; + } + } } -.histogram-chart { +.graph-panel__chart { position: relative; + cursor: crosshair; + flex-grow: 1; } .datapoints-warning { @@ -22,11 +43,12 @@ } .graph-legend { - @include clearfix(); + flex: 0 1 auto; + max-height: 30%; margin: 0 $spacer; text-align: center; - width: calc(100% - $spacer); padding-top: 6px; + position: relative; .popover-content { padding: 0; @@ -89,7 +111,9 @@ display: block; overflow-y: auto; overflow-x: hidden; + height: 100%; padding-bottom: 1px; + padding-right: 5px; } .graph-legend-series { @@ -160,39 +184,6 @@ } } -.graph-legend-rightside { - - &.graph-wrapper { - display: table; - width: 100%; - } - - .graph-canvas-wrapper { - display: table-cell; - width: 100%; - position: relative; - } - - .graph-legend-wrapper { - display: table-cell; - vertical-align: top; - position: relative; - left: 4px; - } - - .graph-legend { - margin: 0 0 0 1rem; - } - - .graph-legend-series { - display: block; - padding-left: 0px; - } - - .graph-legend-table .graph-legend-series { - display: table-row; - } -} .graph-legend-series-hidden { .graph-legend-value, diff --git a/public/sass/components/_panel_pluginlist.scss b/public/sass/components/_panel_pluginlist.scss index f000b795dc1..2c1b074898b 100644 --- a/public/sass/components/_panel_pluginlist.scss +++ b/public/sass/components/_panel_pluginlist.scss @@ -18,7 +18,7 @@ } .pluginlist-image { - width: 20px; + width: 17px; } .pluginlist-title { diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 240cfe80e5d..6c23f51aaa0 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -120,8 +120,9 @@ display: flex; flex-grow: 1; - &:hover { - color: $text-color-weak; + &:hover, &.selected { + color: $link-hover-color; + .search-section__header__toggle { background: $tight-form-func-bg; color: $link-hover-color; @@ -129,12 +130,9 @@ } } -.search-section__header__with-checkbox { - display: flex; -} - .search-section__header__icon { - padding: 5px 10px; + padding: 2px 10px; + width: 35px; } .search-section__header__toggle { @@ -145,14 +143,6 @@ flex-grow: 1; } -.search-item__with-checkbox { - display: flex; - - .search-item { - margin: 1px 3px; - } -} - .search-item { @include list-item(); @include left-brand-border(); @@ -163,11 +153,8 @@ white-space: nowrap; padding: 0px; - &:hover { + &:hover, &.selected { @include left-brand-border-gradient(); - } - - &.selected { background: $list-item-hover-bg; } } @@ -230,3 +217,18 @@ padding: $spacer*2 $spacer; background: $panel-bg; } + +@include media-breakpoint-down(xs) { + + .search-container { + left: 0; + } + + .search-dropdown__col_2 { + display: none; + } + + .search-item__tags { + display: none; + } +} diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index 1dedb062183..bfcfbf900fe 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -11,7 +11,6 @@ .shortcut-table-category-header { font-weight: normal; font-size: $font-size-h6; - font-style: italic; text-align: left; } diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 3ce22688280..05882d75fc9 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -4,25 +4,42 @@ flex-flow: column; flex-direction: column; width: $side-menu-width; - background: $navbarBackground; z-index: $zindex-sidemenu; a:focus { text-decoration: none; } + + .sidemenu__logo_small_breakpoint { + display: none; + } + + .sidemenu__close { + display: none; + } + } -.sidemenu-open { +// body class that hides sidemenu +.sidemenu-hidden { .sidemenu { - background: $side-menu-bg; - position: initial; - height: auto; - box-shadow: $side-menu-shadow; - position: relative; - z-index: $zindex-sidemenu; + display: none; } - .sidemenu__top, - .sidemenu__bottom { - display: block; +} + +@include media-breakpoint-up(sm) { + .sidemenu-open { + .sidemenu { + background: $side-menu-bg; + position: initial; + height: auto; + box-shadow: $side-menu-shadow; + position: relative; + z-index: $zindex-sidemenu; + } + .sidemenu__top, + .sidemenu__bottom { + display: block; + } } } @@ -41,21 +58,23 @@ position: relative; @include left-brand-border(); - &.active, - &:hover { - background-color: $side-menu-item-hover-bg; - @include left-brand-border-gradient(); + @include media-breakpoint-up(sm) { + &.active, + &:hover { + background-color: $side-menu-item-hover-bg; + @include left-brand-border-gradient(); - .dropdown-menu { - margin: 0; - display: block; - opacity: 0; - top: 0px; - // important to overlap it otherwise it can be hidden - // again by the mouse getting outside the hover space - left: $side-menu-width - 2px; - @include animation('dropdown-anim 150ms ease-in-out 100ms forwards'); - z-index: $zindex-sidemenu; + .dropdown-menu { + margin: 0; + display: block; + opacity: 0; + top: 0px; + // important to overlap it otherwise it can be hidden + // again by the mouse getting outside the hover space + left: $side-menu-width - 2px; + @include animation('dropdown-anim 150ms ease-in-out 100ms forwards'); + z-index: $zindex-sidemenu; + } } } } @@ -99,7 +118,7 @@ display: inline-block; .fa, .icon-gf, .gicon { - color: $link-color; + color: $side-menu-link-color; position: relative; opacity: .7; font-size: 130%; @@ -124,6 +143,7 @@ white-space: nowrap; background-color: $side-menu-item-hover-bg; font-size: 17px; + color: #ebedf2; } li.sidemenu-org-switcher { @@ -170,3 +190,88 @@ li.sidemenu-org-switcher { } } +@include media-breakpoint-down(xs) { + .sidemenu-open { + .navbar { + padding-left: 60px !important; + } + } + + .sidemenu-open--xs { + .sidemenu { + width: 100%; + background: $side-menu-bg; + position: initial; + height: auto; + box-shadow: $side-menu-shadow; + position: relative; + z-index: $zindex-sidemenu; + } + + .sidemenu__close { + display: block; + font-size: $font-size-md; + position: relative; + top: -3px; + } + + .sidemenu__top, + .sidemenu__bottom { + display: block; + } + } + + .sidemenu { + .sidemenu__logo { + display: none; + } + + .sidemenu__logo_small_breakpoint { + padding: 12px 10px 26px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; + + .fa-bars { + font-size: 25px; + } + } + + .sidemenu__top { + padding-top: 0rem; + } + + .side-menu-header { + padding-left: 10px; + } + + .sidemenu-link { + text-align: left; + } + + .sidemenu-icon { + display: none + } + + .dropdown-menu--sidemenu { + display: block; + position: unset; + width: 100%; + float: none; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + + > li > a { + padding-left: 15px; + } + } + + .sidemenu__bottom { + .dropdown-menu--sidemenu { + display: flex; + flex-direction: column-reverse; + } + } + } +} diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index f09e83f8908..b612091d171 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -102,6 +102,51 @@ $switch-height: 1.5rem; } } +.gf-form-switch--transparent { + input + label { + background: transparent; + } + + input + label::before, input + label::after { + background: transparent; + } + + &:hover { + input + label::before { + background: transparent; + } + + input + label::after { + background: transparent; + } + } +} + +.gf-form-switch--search-result__section { + min-width: 3.3rem; + margin-right: -0.3rem; + + input + label { + height: 1.7rem; + } +} + +.gf-form-switch--search-result__item { + min-width: 2.6rem; + + input + label { + height: 2.7rem; + } +} + +.gf-form-switch--search-result-filter-row__checkbox { + min-width: 4.7rem; + + input + label { + height: 2.5rem; + } +} + gf-form-switch[disabled] { .gf-form-label, .gf-form-switch input + label { diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index 9eeadf3d61d..d2c9b24d2a0 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -12,8 +12,10 @@ } .tabbed-view-header { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; @include clearfix(); - @include brand-bottom-border(); } .tabbed-view-title { @@ -24,7 +26,7 @@ .tabbed-view-panel-title { float: left; - padding-top: 1rem; + padding-top: 9px; margin: 0 2rem 0 0; } diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index fee6d2cb7b8..396c3f98880 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -16,7 +16,7 @@ position: relative; display: block; border: solid transparent; - border-width: 2px 1px 1px; + border-width: 0 1px 1px; border-radius: 3px 3px 0 0; i { @@ -31,9 +31,21 @@ &.active, &.active:hover, &.active:focus { - border-color: $orange $dark-4 transparent; + border-color: $orange $tab-border-color transparent; background: $page-bg; color: $link-color; + overflow: hidden; + + &::before { + display: block; + content: ' '; + position: absolute; + left: 0; + right: 0; + height: 2px; + top: 0; + background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); + } } } diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index a1fb558c087..24fbdfa2fcb 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -13,11 +13,13 @@ } .gf-timepicker-dropdown { - margin: -8px -10px 10px 5px; + position: absolute; + top: $navbarHeight; + right: 0; padding: 10px 20px; - float: right; - background-color: $panel-bg; - @include box-shadow($navbarDropdownShadow); + background-color: $search-filter-box-bg; + border-radius: 0 0 0 4px; + box-shadow: $search-shadow; } .gf-timepicker-absolute-section { diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index a698a81f1dc..b3c9a44e658 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -37,6 +37,7 @@ .navbar { box-shadow: none; background: transparent; + padding-left: $side-menu-width + 20px; } .navbar-page-btn { diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 19a4fb1b8e1..35371cc35a9 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -15,8 +15,12 @@ } .page-container { - @extend .container; - padding: 0 $spacer * 2; + margin-left: auto; + margin-right: auto; + padding-left: $spacer*2; + padding-right: $spacer*2; + max-width: 980px; + @include clearfix(); } .scroll-canvas { diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss new file mode 100644 index 00000000000..a174cc8f805 --- /dev/null +++ b/public/sass/mixins/_drop_element.scss @@ -0,0 +1,279 @@ + +@mixin drop-theme($themeName, $theme-bg, $theme-color, $border-color: $theme-bg) { + .drop-element.drop-#{$themeName} { + max-width: 100%; + max-height: 100%; + + .drop-content { + border-radius: $border-radius-lg; + position: relative; + font-family: inherit; + background: $theme-bg; + color: $theme-color; + padding: 0.65rem; + word-wrap: break-word; + max-width: 20rem; + border: 1px solid $border-color; + + &:before { + content: ""; + display: block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-width: $popover-arrow-size; + border-style: solid; + pointer-events: null; + } + } + + &.drop-wide { + .drop-content { + max-width: 40rem; + } + } + + // Centers and middles + + &.drop-element-attached-bottom.drop-element-attached-center .drop-content { + margin-bottom: $popover-arrow-size; + + &:before { + top: 100%; + left: 50%; + margin-left: - $popover-arrow-size; + border-top-color: $border-color; + } + } + + &.drop-element-attached-top.drop-element-attached-center .drop-content { + margin-top: $popover-arrow-size; + + &:before { + bottom: 100%; + left: 50%; + margin-left: - $popover-arrow-size; + border-bottom-color: $border-color; + } + } + + &.drop-element-attached-right.drop-element-attached-middle .drop-content { + margin-right: $popover-arrow-size; + + &:before { + left: 100%; + top: 50%; + margin-top: - $popover-arrow-size; + border-left-color: $border-color; + } + } + + &.drop-element-attached-left.drop-element-attached-middle .drop-content { + margin-left: $popover-arrow-size; + + &:before { + right: 100%; + top: 50%; + margin-top: - $popover-arrow-size; + border-right-color: $border-color; + } + } + + // Target middle/center, element corner + &.drop-element-attached-left.drop-target-attached-center .drop-content { + left: - $popover-arrow-size * 2; + } + + &.drop-element-attached-right.drop-target-attached-center .drop-content { + left: $popover-arrow-size * 2; + } + + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle .drop-content { + margin-top: $popover-arrow-size; + + &:before { + bottom: 100%; + left: $popover-arrow-size; + border-bottom-color: $border-color; + } + } + + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle .drop-content { + margin-top: $popover-arrow-size; + + &:before { + bottom: 100%; + right: $popover-arrow-size; + border-bottom-color: $border-color; + } + } + + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle .drop-content { + margin-bottom: $popover-arrow-size; + + &:before { + top: 100%; + left: $popover-arrow-size; + border-top-color: $border-color; + } + } + + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle .drop-content { + margin-bottom: $popover-arrow-size; + + &:before { + top: 100%; + right: $popover-arrow-size; + border-top-color: $border-color; + } + } + + // Top and bottom corners + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom .drop-content { + margin-top: $popover-arrow-size; + + &:before { + bottom: 100%; + left: $popover-arrow-size; + border-bottom-color: $border-color; + } + } + + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom .drop-content { + margin-top: $popover-arrow-size; + + &:before { + bottom: 100%; + right: $popover-arrow-size; + border-bottom-color: $border-color; + } + } + + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top .drop-content { + margin-bottom: $popover-arrow-size; + + &:before { + top: 100%; + left: $popover-arrow-size; + border-top-color: $border-color; + } + } + + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top .drop-content { + margin-bottom: $popover-arrow-size; + + &:before { + top: 100%; + right: $popover-arrow-size; + border-top-color: $border-color; + } + } + + // Side corners + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left .drop-content { + margin-right: $popover-arrow-size; + + &:before { + top: $popover-arrow-size; + left: 100%; + border-left-color: $border-color; + } + } + + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right .drop-content { + margin-left: $popover-arrow-size; + + &:before { + top: $popover-arrow-size; + right: 100%; + border-right-color: $border-color; + } + } + + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left .drop-content { + margin-right: $popover-arrow-size; + + &:before { + bottom: $popover-arrow-size; + left: 100%; + border-left-color: $border-color; + } + } + + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right .drop-content { + margin-left: $popover-arrow-size; + + &:before { + bottom: $popover-arrow-size; + right: 100%; + border-right-color: $border-color; + } + } + } +} + +@mixin drop-animation-scale($themePrefix: "drop", $themeName: "default", $attachmentOffset: 0, $easing: "linear") { + .#{$themePrefix}-element.#{$themePrefix}-#{$themeName} { + transform: translateZ(0); + transition: opacity 100ms; + opacity: 0; + .#{$themePrefix}-content { + transition: transform 0.2s $easing; + transform: scale(0.8) translateZ(0); + } + &.#{$themePrefix}-open { + display: none; + } + &.#{$themePrefix}-open-transitionend { + display: block; + } + &.#{$themePrefix}-after-open { + transition: none; + opacity: 1; + .#{$themePrefix}-content { + transform: scale(1) translateZ(0); + } + } + // Centers and middles + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + transform-origin: 50% calc(100% + #{$attachmentOffset}); + } + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + transform-origin: 50% (-$attachmentOffset); + } + &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + transform-origin: calc(100% + #{$attachmentOffset}) 50%; + } + &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + transform-origin: -($attachmentOffset 50%); + } + // Top and bottom corners + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content { + transform-origin: 0 (-$attachmentOffset); + } + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content { + transform-origin: 100% (-$attachmentOffset); + } + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-top .#{$themePrefix}-content { + transform-origin: 0 calc(100% + #{$attachmentOffset}); + } + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-top .#{$themePrefix}-content { + transform-origin: 100% calc(100% + #{$attachmentOffset}); + } + // Side corners + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content { + transform-origin: calc(100% + #{$attachmentOffset}) 0; + } + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content { + transform-origin: (-$attachmentOffset) 0; + } + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content { + transform-origin: calc(100% + #{$attachmentOffset}) 100%; + } + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content { + transform-origin: (-$attachmentOffset) 100%; + } + } +} + diff --git a/public/sass/pages/_errorpage.scss b/public/sass/pages/_errorpage.scss index e18306ea05a..dac2768e89b 100644 --- a/public/sass/pages/_errorpage.scss +++ b/public/sass/pages/_errorpage.scss @@ -3,6 +3,11 @@ // Layout // +.error-container { + display: flex; + flex-direction: row; +} + .error-row { display: flex; flex-direction: row; @@ -22,7 +27,7 @@ .info-box { width: 38%; - padding: 2rem 1rem 6rem; + padding: 2rem 1rem 2rem; } .graph-percentage {padding: 0 0 1.5rem;} @@ -58,3 +63,31 @@ } .graph-text {margin: 0;} + +@include media-breakpoint-down(sm) { + .graph-box { + width: 50%; + } + + .info-box { + width: 50%; + } +} + +@include media-breakpoint-down(xs) { + .error-container { + flex-direction: column; + } + + .graph-box { + width: 100%; + } + + .info-box { + width: 100%; + } + + .error-full-width { + width: 100%; + } +} diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 3c54224c4cc..2b3426b45b5 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -8,12 +8,6 @@ padding-top: $spacer * 5; /* BAD HACK - experiement to see how it looks */ } -.page-login { - .sidemenu { - display: none; - } -} - .login-form { display: inline-block; max-width: 24rem; diff --git a/public/views/index.template.html b/public/views/index.template.html index e022cc4873b..461bc0aa804 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -1,109 +1,114 @@ - - - - - - Grafana + + + + + - + Grafana - + - - + - + + - - + - + + -
    -
    -
    -
    -
    {{alert.title}}
    -
    -
    - -
    -
    + -
    -
    -
    - - -
    +
    +
    +
    + +
    +
    +
    {{alert.title}}
    +
    +
    +
    - +
    - + [[if .GoogleTagManagerId]] + + + + + + [[end]] - [[if .GoogleTagManagerId]] - - - - - - [[end]] + -