Browse Source

Merge remote-tracking branch 'upstream/master' into mysql_query_builder

Sven Klemm 7 years ago
parent
commit
2f254187fc
100 changed files with 2131 additions and 668 deletions
  1. 86 4
      .circleci/config.yml
  2. 3 0
      CHANGELOG.md
  3. 3 0
      conf/defaults.ini
  4. 29 2
      docs/sources/features/datasources/stackdriver.md
  5. 1 1
      docs/sources/installation/debian.md
  6. 1 1
      pkg/api/api.go
  7. 7 3
      pkg/api/frontendsettings.go
  8. 1 1
      pkg/api/index.go
  9. 18 2
      pkg/api/pluginproxy/ds_auth_provider.go
  10. 5 0
      pkg/setting/setting.go
  11. 24 0
      pkg/tsdb/stackdriver/ensure_default_project.go
  12. 33 2
      pkg/tsdb/stackdriver/stackdriver.go
  13. 18 27
      public/app/app.ts
  14. 1 0
      public/app/core/components/scroll/scroll.ts
  15. 2 1
      public/app/core/config.ts
  16. 3 0
      public/app/core/constants.ts
  17. 0 2
      public/app/core/core.ts
  18. 17 1
      public/app/core/core_module.ts
  19. 11 6
      public/app/core/directives/dash_class.ts
  20. 9 2
      public/app/core/reducers/location.ts
  21. 14 20
      public/app/core/services/dynamic_directive_srv.ts
  22. 2 2
      public/app/core/services/keybindingSrv.ts
  23. 0 1
      public/app/features/dashboard/all.ts
  24. 9 12
      public/app/features/dashboard/dashboard_ctrl.ts
  25. 37 0
      public/app/features/dashboard/dashboard_model.ts
  26. 4 7
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  27. 22 24
      public/app/features/dashboard/dashgrid/DashboardGrid.tsx
  28. 1 3
      public/app/features/dashboard/dashgrid/DashboardGridDirective.ts
  29. 134 27
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  30. 7 17
      public/app/features/dashboard/dashgrid/DashboardRow.tsx
  31. 151 0
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  32. 84 0
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  33. 0 7
      public/app/features/dashboard/dashgrid/PanelContainer.ts
  34. 121 0
      public/app/features/dashboard/dashgrid/PanelEditor.tsx
  35. 83 0
      public/app/features/dashboard/dashgrid/PanelHeader.tsx
  36. 53 0
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  37. 69 0
      public/app/features/dashboard/dashgrid/VizTypePicker.tsx
  38. 2 0
      public/app/features/dashboard/dashnav/dashnav.ts
  39. 45 4
      public/app/features/dashboard/panel_model.ts
  40. 1 1
      public/app/features/dashboard/settings/settings.ts
  41. 1 2
      public/app/features/dashboard/share_snapshot_ctrl.ts
  42. 17 7
      public/app/features/dashboard/specs/AddPanelPanel.test.tsx
  43. 4 9
      public/app/features/dashboard/specs/DashboardRow.test.tsx
  44. 1 1
      public/app/features/dashboard/specs/exporter.test.ts
  45. 7 7
      public/app/features/dashboard/specs/viewstate_srv.test.ts
  46. 2 2
      public/app/features/dashboard/submenu/submenu.ts
  47. 21 12
      public/app/features/dashboard/time_srv.ts
  48. 1 1
      public/app/features/dashboard/timepicker/settings.html
  49. 2 1
      public/app/features/dashboard/timepicker/timepicker.ts
  50. 27 85
      public/app/features/dashboard/view_state_srv.ts
  51. 1 28
      public/app/features/panel/metrics_panel_ctrl.ts
  52. 34 2
      public/app/features/panel/metrics_tab.ts
  53. 17 20
      public/app/features/panel/panel_ctrl.ts
  54. 43 44
      public/app/features/panel/panel_directive.ts
  55. 18 10
      public/app/features/panel/panel_editor_tab.ts
  56. 0 15
      public/app/features/panel/panel_header.ts
  57. 0 4
      public/app/features/panel/partials/metrics_tab.html
  58. 73 0
      public/app/features/panel/viz_tab.ts
  59. 4 0
      public/app/features/plugins/built_in_plugins.ts
  60. 17 1
      public/app/features/plugins/datasource_srv.ts
  61. 8 6
      public/app/features/plugins/plugin_component.ts
  62. 3 1
      public/app/features/plugins/plugin_loader.ts
  63. 7 4
      public/app/features/templating/specs/variable_srv.test.ts
  64. 3 2
      public/app/features/templating/specs/variable_srv_init.test.ts
  65. 9 9
      public/app/features/templating/variable_srv.ts
  66. 2 3
      public/app/partials/dashboard.html
  67. 5 1
      public/app/plugins/datasource/cloudwatch/datasource.ts
  68. 3 2
      public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts
  69. 3 4
      public/app/plugins/datasource/elasticsearch/bucket_agg.ts
  70. 3 4
      public/app/plugins/datasource/elasticsearch/metric_agg.ts
  71. 2 2
      public/app/plugins/datasource/graphite/add_graphite_func.ts
  72. 2 2
      public/app/plugins/datasource/graphite/func_editor.ts
  73. 15 1
      public/app/plugins/datasource/stackdriver/config_ctrl.ts
  74. 109 86
      public/app/plugins/datasource/stackdriver/datasource.ts
  75. 42 23
      public/app/plugins/datasource/stackdriver/partials/config.html
  76. 3 4
      public/app/plugins/datasource/stackdriver/partials/query.editor.html
  77. 1 4
      public/app/plugins/datasource/stackdriver/plugin.json
  78. 3 3
      public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
  79. 2 8
      public/app/plugins/datasource/stackdriver/query_ctrl.ts
  80. 16 6
      public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
  81. 10 45
      public/app/plugins/datasource/stackdriver/specs/datasource.test.ts
  82. 0 1
      public/app/plugins/datasource/testdata/datasource.ts
  83. 6 5
      public/app/plugins/panel/graph/legend.ts
  84. 1 1
      public/app/plugins/panel/graph/module.ts
  85. 2 2
      public/app/plugins/panel/graph/series_overrides_ctrl.ts
  86. 5 0
      public/app/plugins/panel/graph2/README.md
  87. 26 0
      public/app/plugins/panel/graph2/img/icn-text-panel.svg
  88. 43 0
      public/app/plugins/panel/graph2/module.tsx
  89. 17 0
      public/app/plugins/panel/graph2/plugin.json
  90. 3 5
      public/app/plugins/panel/heatmap/color_legend.ts
  91. 5 0
      public/app/plugins/panel/text2/README.md
  92. 186 0
      public/app/plugins/panel/text2/img/icn-graph-panel.svg
  93. 14 0
      public/app/plugins/panel/text2/module.tsx
  94. 19 0
      public/app/plugins/panel/text2/plugin.json
  95. 7 3
      public/app/routes/GrafanaCtrl.ts
  96. 24 0
      public/app/types/index.ts
  97. 1 0
      public/app/types/location.ts
  98. 7 0
      public/app/types/panel.ts
  99. 22 0
      public/app/types/plugins.ts
  100. 91 0
      public/app/types/series.ts

+ 86 - 4
.circleci/config.yml

@@ -238,8 +238,17 @@ jobs:
     steps:
     steps:
       - checkout
       - checkout
       - run:
       - run:
-          name: build, test and package grafana enterprise
-          command: './scripts/build/build_enterprise.sh'
+          name: prepare build tools
+          command: '/tmp/bootstrap.sh'
+      - run:
+          name: checkout enterprise
+          command: './scripts/build/prepare-enterprise.sh'
+      - run:
+          name: test enterprise
+          command: 'go test ./pkg/extensions/...'
+      - run:
+          name: build and package enterprise
+          command: './scripts/build/build.sh -enterprise'
       - run:
       - run:
           name: sign packages
           name: sign packages
           command: './scripts/build/sign_packages.sh'
           command: './scripts/build/sign_packages.sh'
@@ -254,6 +263,53 @@ jobs:
           paths:
           paths:
             - enterprise-dist/grafana-enterprise*
             - enterprise-dist/grafana-enterprise*
 
 
+  build-all-enterprise:
+    docker:
+    - image: grafana/build-container:1.2.0
+    working_directory: /go/src/github.com/grafana/grafana
+    steps:
+    - checkout
+    - run:
+        name: prepare build tools
+        command: '/tmp/bootstrap.sh'
+    - run:
+        name: checkout enterprise
+        command: './scripts/build/prepare-enterprise.sh'
+    - restore_cache:
+        key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }}
+    - run:
+        name: download phantomjs binaries
+        command: './scripts/build/download-phantomjs.sh'
+    - save_cache:
+        key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }}
+        paths:
+        - /tmp/phantomjs
+    - run:
+        name: test enterprise
+        command: 'go test ./pkg/extensions/...'
+    - run:
+        name: build and package grafana
+        command: './scripts/build/build-all.sh -enterprise'
+    - run:
+        name: sign packages
+        command: './scripts/build/sign_packages.sh'
+    - run:
+        name: verify signed packages
+        command: |
+          mkdir -p ~/.rpmdb/pubkeys
+          curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
+          ./scripts/build/verify_signed_packages.sh dist/*.rpm
+    - run:
+        name: sha-sum packages
+        command: 'go run build.go sha-dist'
+    - run:
+        name: move enterprise packages into their own folder
+        command: 'mv dist enterprise-dist'
+    - persist_to_workspace:
+        root: .
+        paths:
+        - enterprise-dist/grafana-enterprise*
+
   deploy-enterprise-master:
   deploy-enterprise-master:
     docker:
     docker:
       - image: circleci/python:2.7-stretch
       - image: circleci/python:2.7-stretch
@@ -267,6 +323,19 @@ jobs:
           name: deploy to s3
           name: deploy to s3
           command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
           command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
 
 
+  deploy-enterprise-release:
+    docker:
+    - image: circleci/python:2.7-stretch
+    steps:
+    - attach_workspace:
+        at: .
+    - run:
+        name: install awscli
+        command: 'sudo pip install awscli'
+    - run:
+        name: deploy to s3
+        command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
+
   deploy-master:
   deploy-master:
     docker:
     docker:
       - image: circleci/python:2.7-stretch
       - image: circleci/python:2.7-stretch
@@ -313,7 +382,7 @@ workflows:
     jobs:
     jobs:
       - build-all:
       - build-all:
           filters: *filter-only-master
           filters: *filter-only-master
-      - build-enterprise:
+      - build-all-enterprise:
           filters: *filter-only-master
           filters: *filter-only-master
       - codespell:
       - codespell:
           filters: *filter-only-master
           filters: *filter-only-master
@@ -356,13 +425,15 @@ workflows:
             - gometalinter
             - gometalinter
             - mysql-integration-test
             - mysql-integration-test
             - postgres-integration-test
             - postgres-integration-test
-            - build-enterprise
+            - build-all-enterprise
           filters: *filter-only-master
           filters: *filter-only-master
 
 
   release:
   release:
     jobs:
     jobs:
       - build-all:
       - build-all:
           filters: *filter-only-release
           filters: *filter-only-release
+      - build-all-enterprise:
+          filters: *filter-only-release
       - codespell:
       - codespell:
           filters: *filter-only-release
           filters: *filter-only-release
       - gometalinter:
       - gometalinter:
@@ -385,6 +456,17 @@ workflows:
             - mysql-integration-test
             - mysql-integration-test
             - postgres-integration-test
             - postgres-integration-test
           filters: *filter-only-release
           filters: *filter-only-release
+      - deploy-enterprise-release:
+          requires:
+            - build-all
+            - build-all-enterprise
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters: *filter-only-release
       - grafana-docker-release:
       - grafana-docker-release:
           requires:
           requires:
             - build-all
             - build-all

+ 3 - 0
CHANGELOG.md

@@ -6,6 +6,7 @@
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
 * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
 * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
 * **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
 * **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
 
 
 ### Minor
 ### Minor
 
 
@@ -19,8 +20,10 @@
 
 
 # 5.3.2 (unreleased)
 # 5.3.2 (unreleased)
 
 
+* **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
 * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda)
+* **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda)
 * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f)
 * **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f)
 
 
 # 5.3.1 (2018-10-16)
 # 5.3.1 (2018-10-16)

+ 3 - 0
conf/defaults.ini

@@ -554,3 +554,6 @@ container_name =
 # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
 # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
 server_url =
 server_url =
 callback_url =
 callback_url =
+
+[panels]
+enable_alpha = false

+ 29 - 2
docs/sources/features/datasources/stackdriver.md

@@ -35,7 +35,9 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat
 
 
 ## Authentication
 ## Authentication
 
 
-### Service Account Credentials - Private Key File
+There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by automatically retrieving credentials from Google metadata server. The latter option is only available when running Grafana on GCE virtual machine.
+
+### Using a Google Service Account Key File
 
 
 To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
 To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
 
 
@@ -74,6 +76,16 @@ Click on the links above and click the `Enable` button:
 
 
     {{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
     {{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
 
 
+### Using GCE Default Service Account
+
+If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that need to be met.
+
+1. First of all, you need to create a Service Account that can be used by the GCE virtual machine. See detailed instructions on how to do that [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount).
+2. Make sure the GCE virtual machine instance is being run as the service account that you just created. See instructions [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using).
+3. Allow access to the `Stackdriver Monitoring API` scope. See instructions [here](changeserviceaccountandscopes).
+
+Read more about creating and enabling service accounts for GCE VM instances [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
+
 ## Metric Query Editor
 ## Metric Query Editor
 
 
 {{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}}
 {{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}}
@@ -194,7 +206,7 @@ Example Result: `monitoring.googleapis.com/uptime_check/http_status has this val
 
 
 It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
 It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
 
 
-Here is a provisioning example for this datasource.
+Here is a provisioning example using the JWT (Service Account key file) authentication type.
 
 
 ```yaml
 ```yaml
 apiVersion: 1
 apiVersion: 1
@@ -206,6 +218,8 @@ datasources:
     jsonData:
     jsonData:
       tokenUri: https://oauth2.googleapis.com/token
       tokenUri: https://oauth2.googleapis.com/token
       clientEmail: stackdriver@myproject.iam.gserviceaccount.com
       clientEmail: stackdriver@myproject.iam.gserviceaccount.com
+      authenticationType: jwt
+      defaultProject: my-project-name
     secureJsonData:
     secureJsonData:
       privateKey: |
       privateKey: |
         -----BEGIN PRIVATE KEY-----
         -----BEGIN PRIVATE KEY-----
@@ -214,3 +228,16 @@ datasources:
         yA+23427282348234=
         yA+23427282348234=
         -----END PRIVATE KEY-----
         -----END PRIVATE KEY-----
 ```
 ```
+
+Here is a provisioning example using GCE Default Service Account authentication.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Stackdriver
+    type: stackdriver
+    access: proxy
+    jsonData:
+      authenticationType: gce
+```

+ 1 - 1
docs/sources/installation/debian.md

@@ -28,7 +28,7 @@ installation.
 ```bash
 ```bash
 wget <debian package url>
 wget <debian package url>
 sudo apt-get install -y adduser libfontconfig
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.1.4_amd64.deb
+sudo dpkg -i grafana_<version>_amd64.deb
 ```
 ```
 
 
 Example:
 Example:

+ 1 - 1
pkg/api/api.go

@@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
 			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
 			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
 		}, reqOrgAdmin)
 
 
-		apiRoute.Get("/frontend/settings/", GetFrontendSettings)
+		apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
 		apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
 		apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
 		apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
 		apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
 
 

+ 7 - 3
pkg/api/frontendsettings.go

@@ -11,7 +11,7 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
-func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
+func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 	orgDataSources := make([]*m.DataSource, 0)
 	orgDataSources := make([]*m.DataSource, 0)
 
 
 	if c.OrgId != 0 {
 	if c.OrgId != 0 {
@@ -133,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 
 
 	panels := map[string]interface{}{}
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
 	for _, panel := range enabledPlugins.Panels {
+		if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
+			continue
+		}
+
 		panels[panel.Id] = map[string]interface{}{
 		panels[panel.Id] = map[string]interface{}{
 			"module":       panel.Module,
 			"module":       panel.Module,
 			"baseUrl":      panel.BaseUrl,
 			"baseUrl":      panel.BaseUrl,
@@ -196,8 +200,8 @@ func getPanelSort(id string) int {
 	return sort
 	return sort
 }
 }
 
 
-func GetFrontendSettings(c *m.ReqContext) {
-	settings, err := getFrontendSettingsMap(c)
+func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
+	settings, err := hs.getFrontendSettingsMap(c)
 	if err != nil {
 	if err != nil {
 		c.JsonApiErr(400, "Failed to get frontend settings", err)
 		c.JsonApiErr(400, "Failed to get frontend settings", err)
 		return
 		return

+ 1 - 1
pkg/api/index.go

@@ -18,7 +18,7 @@ const (
 )
 )
 
 
 func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
-	settings, err := getFrontendSettingsMap(c)
+	settings, err := hs.getFrontendSettingsMap(c)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 18 - 2
pkg/api/pluginproxy/ds_auth_provider.go

@@ -12,6 +12,7 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
+	"golang.org/x/oauth2/google"
 )
 )
 
 
 //ApplyRoute should use the plugin route data to set auth headers and custom headers
 //ApplyRoute should use the plugin route data to set auth headers and custom headers
@@ -54,15 +55,30 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
 		}
 		}
 	}
 	}
 
 
-	if route.JwtTokenAuth != nil {
+	authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt")
+	if route.JwtTokenAuth != nil && authenticationType == "jwt" {
 		if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
 		if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
 			logger.Error("Failed to get access token", "error", err)
 			logger.Error("Failed to get access token", "error", err)
 		} else {
 		} else {
 			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
 			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
 		}
 		}
 	}
 	}
-	logger.Info("Requesting", "url", req.URL.String())
 
 
+	if authenticationType == "gce" {
+		tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...)
+		if err != nil {
+			logger.Error("Failed to get default token from meta data server", "error", err)
+		} else {
+			token, err := tokenSrc.Token()
+			if err != nil {
+				logger.Error("Failed to get default access token from meta data server", "error", err)
+			} else {
+				req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
+			}
+		}
+	}
+
+	logger.Info("Requesting", "url", req.URL.String())
 }
 }
 
 
 func interpolateString(text string, data templateData) (string, error) {
 func interpolateString(text string, data templateData) (string, error) {

+ 5 - 0
pkg/setting/setting.go

@@ -213,6 +213,8 @@ type Cfg struct {
 	TempDataLifetime time.Duration
 	TempDataLifetime time.Duration
 
 
 	MetricsEndpointEnabled bool
 	MetricsEndpointEnabled bool
+
+	EnableAlphaPanels bool
 }
 }
 
 
 type CommandLineArgs struct {
 type CommandLineArgs struct {
@@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	explore := iniFile.Section("explore")
 	explore := iniFile.Section("explore")
 	ExploreEnabled = explore.Key("enabled").MustBool(false)
 	ExploreEnabled = explore.Key("enabled").MustBool(false)
 
 
+	panels := iniFile.Section("panels")
+	cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
+
 	cfg.readSessionConfig()
 	cfg.readSessionConfig()
 	cfg.readSmtpSettings()
 	cfg.readSmtpSettings()
 	cfg.readQuotaSettings()
 	cfg.readQuotaSettings()

+ 24 - 0
pkg/tsdb/stackdriver/ensure_default_project.go

@@ -0,0 +1,24 @@
+package stackdriver
+
+import (
+	"context"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/tsdb"
+)
+
+func (e *StackdriverExecutor) ensureDefaultProject(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
+	queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId}
+	result := &tsdb.Response{
+		Results: make(map[string]*tsdb.QueryResult),
+	}
+	defaultProject, err := e.getDefaultProject(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	e.dsInfo.JsonData.Set("defaultProject", defaultProject)
+	queryResult.Meta.Set("defaultProject", defaultProject)
+	result.Results[tsdbQuery.Queries[0].RefId] = queryResult
+	return result, nil
+}

+ 33 - 2
pkg/tsdb/stackdriver/stackdriver.go

@@ -16,6 +16,7 @@ import (
 	"time"
 	"time"
 
 
 	"golang.org/x/net/context/ctxhttp"
 	"golang.org/x/net/context/ctxhttp"
+	"golang.org/x/oauth2/google"
 
 
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
 	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/null"
@@ -34,6 +35,11 @@ var (
 	metricNameFormat *regexp.Regexp
 	metricNameFormat *regexp.Regexp
 )
 )
 
 
+const (
+	gceAuthentication string = "gce"
+	jwtAuthentication string = "jwt"
+)
+
 // StackdriverExecutor executes queries for the Stackdriver datasource
 // StackdriverExecutor executes queries for the Stackdriver datasource
 type StackdriverExecutor struct {
 type StackdriverExecutor struct {
 	httpClient *http.Client
 	httpClient *http.Client
@@ -71,6 +77,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour
 	switch queryType {
 	switch queryType {
 	case "annotationQuery":
 	case "annotationQuery":
 		result, err = e.executeAnnotationQuery(ctx, tsdbQuery)
 		result, err = e.executeAnnotationQuery(ctx, tsdbQuery)
+	case "ensureDefaultProjectQuery":
+		result, err = e.ensureDefaultProject(ctx, tsdbQuery)
 	case "timeSeriesQuery":
 	case "timeSeriesQuery":
 		fallthrough
 		fallthrough
 	default:
 	default:
@@ -85,6 +93,16 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu
 		Results: make(map[string]*tsdb.QueryResult),
 		Results: make(map[string]*tsdb.QueryResult),
 	}
 	}
 
 
+	authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
+	if authenticationType == gceAuthentication {
+		defaultProject, err := e.getDefaultProject(ctx)
+		if err != nil {
+			return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err)
+		}
+
+		e.dsInfo.JsonData.Set("defaultProject", defaultProject)
+	}
+
 	queries, err := e.buildQueries(tsdbQuery)
 	queries, err := e.buildQueries(tsdbQuery)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -550,8 +568,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
 	if !ok {
 	if !ok {
 		return nil, errors.New("Unable to find datasource plugin Stackdriver")
 		return nil, errors.New("Unable to find datasource plugin Stackdriver")
 	}
 	}
-	projectName := dsInfo.JsonData.Get("defaultProject").MustString()
-	proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries")
 
 
 	var stackdriverRoute *plugins.AppPluginRoute
 	var stackdriverRoute *plugins.AppPluginRoute
 	for _, route := range plugin.Routes {
 	for _, route := range plugin.Routes {
@@ -561,7 +577,22 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
 		}
 		}
 	}
 	}
 
 
+	projectName := dsInfo.JsonData.Get("defaultProject").MustString()
+	proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries")
+
 	pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo)
 	pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo)
 
 
 	return req, nil
 	return req, nil
 }
 }
+
+func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) {
+	authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
+	if authenticationType == gceAuthentication {
+		defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read")
+		if err != nil {
+			return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err)
+		}
+		return defaultCredentials.ProjectID, nil
+	}
+	return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil
+}

+ 18 - 27
public/app/app.ts

@@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => {
   return array;
   return array;
 };
 };
 
 
-import { coreModule, registerAngularDirectives } from './core/core';
-import { setupAngularRoutes } from './routes/routes';
+import { coreModule, angularModules } from 'app/core/core_module';
+import { registerAngularDirectives } from 'app/core/core';
+import { setupAngularRoutes } from 'app/routes/routes';
+
+import 'app/routes/GrafanaCtrl';
+import 'app/features/all';
 
 
 // import symlinked extensions
 // import symlinked extensions
 const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
 const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
@@ -109,39 +113,26 @@ export class GrafanaApp {
       'react',
       'react',
     ];
     ];
 
 
-    const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
-
-    _.each(moduleTypes, type => {
-      const moduleName = 'grafana.' + type;
-      this.useModule(angular.module(moduleName, []));
-    });
-
     // makes it possible to add dynamic stuff
     // makes it possible to add dynamic stuff
-    this.useModule(coreModule);
+    _.each(angularModules, m => {
+      this.useModule(m);
+    });
 
 
     // register react angular wrappers
     // register react angular wrappers
     coreModule.config(setupAngularRoutes);
     coreModule.config(setupAngularRoutes);
     registerAngularDirectives();
     registerAngularDirectives();
 
 
-    const preBootRequires = [import('app/features/all')];
+    // disable tool tip animation
+    $.fn.tooltip.defaults.animation = false;
 
 
-    Promise.all(preBootRequires)
-      .then(() => {
-        // disable tool tip animation
-        $.fn.tooltip.defaults.animation = false;
-
-        // bootstrap the app
-        angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
-          _.each(this.preBootModules, module => {
-            _.extend(module, this.registerFunctions);
-          });
-
-          this.preBootModules = null;
-        });
-      })
-      .catch(err => {
-        console.log('Application boot failed:', err);
+    // bootstrap the app
+    angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
+      _.each(this.preBootModules, module => {
+        _.extend(module, this.registerFunctions);
       });
       });
+
+      this.preBootModules = null;
+    });
   }
   }
 }
 }
 
 

+ 1 - 0
public/app/core/components/scroll/scroll.ts

@@ -18,6 +18,7 @@ export function geminiScrollbar() {
       let scrollRoot = elem.parent();
       let scrollRoot = elem.parent();
       const scroller = elem;
       const scroller = elem;
 
 
+      console.log('scroll');
       if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
       if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
         scrollRoot = scroller;
         scrollRoot = scroller;
       }
       }

+ 2 - 1
public/app/core/config.ts

@@ -1,4 +1,5 @@
 import _ from 'lodash';
 import _ from 'lodash';
+import { PanelPlugin } from 'app/types/plugins';
 
 
 export interface BuildInfo {
 export interface BuildInfo {
   version: string;
   version: string;
@@ -9,7 +10,7 @@ export interface BuildInfo {
 
 
 export class Settings {
 export class Settings {
   datasources: any;
   datasources: any;
-  panels: any;
+  panels: PanelPlugin[];
   appSubUrl: string;
   appSubUrl: string;
   windowTitlePrefix: string;
   windowTitlePrefix: string;
   buildInfo: BuildInfo;
   buildInfo: BuildInfo;

+ 3 - 0
public/app/core/constants.ts

@@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250;
 export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
 export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
 
 
 export const LS_PANEL_COPY_KEY = 'panel-copy';
 export const LS_PANEL_COPY_KEY = 'panel-copy';
+
+export const DASHBOARD_TOOLBAR_HEIGHT = 55;
+export const DASHBOARD_TOP_PADDING = 20;

+ 0 - 2
public/app/core/core.ts

@@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker';
 import './services/search_srv';
 import './services/search_srv';
 import './services/ng_react';
 import './services/ng_react';
 
 
-import { grafanaAppDirective } from './components/grafana_app';
 import { searchDirective } from './components/search/search';
 import { searchDirective } from './components/search/search';
 import { infoPopover } from './components/info_popover';
 import { infoPopover } from './components/info_popover';
 import { navbarDirective } from './components/navbar/navbar';
 import { navbarDirective } from './components/navbar/navbar';
@@ -60,7 +59,6 @@ export {
   registerAngularDirectives,
   registerAngularDirectives,
   arrayJoin,
   arrayJoin,
   coreModule,
   coreModule,
-  grafanaAppDirective,
   navbarDirective,
   navbarDirective,
   searchDirective,
   searchDirective,
   liveSrv,
   liveSrv,

+ 17 - 1
public/app/core/core_module.ts

@@ -1,2 +1,18 @@
 import angular from 'angular';
 import angular from 'angular';
-export default angular.module('grafana.core', ['ngRoute']);
+
+const coreModule = angular.module('grafana.core', ['ngRoute']);
+
+// legacy modules
+const angularModules = [
+  coreModule,
+  angular.module('grafana.controllers', []),
+  angular.module('grafana.directives', []),
+  angular.module('grafana.factories', []),
+  angular.module('grafana.services', []),
+  angular.module('grafana.filters', []),
+  angular.module('grafana.routes', []),
+];
+
+export { angularModules, coreModule };
+
+export default coreModule;

+ 11 - 6
public/app/core/directives/dash_class.ts

@@ -2,16 +2,21 @@ import _ from 'lodash';
 import coreModule from '../core_module';
 import coreModule from '../core_module';
 
 
 /** @ngInject */
 /** @ngInject */
-export function dashClass() {
+function dashClass($timeout) {
   return {
   return {
     link: ($scope, elem) => {
     link: ($scope, elem) => {
-      $scope.onAppEvent('panel-fullscreen-enter', () => {
-        elem.toggleClass('panel-in-fullscreen', true);
+      $scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
+        console.log('view-mode-changed', panel.fullscreen);
+        if (panel.fullscreen) {
+          elem.addClass('panel-in-fullscreen');
+        } else {
+          $timeout(() => {
+            elem.removeClass('panel-in-fullscreen');
+          });
+        }
       });
       });
 
 
-      $scope.onAppEvent('panel-fullscreen-exit', () => {
-        elem.toggleClass('panel-in-fullscreen', false);
-      });
+      elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
 
 
       $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
       $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
         if (newValue) {
         if (newValue) {

+ 9 - 2
public/app/core/reducers/location.ts

@@ -1,6 +1,7 @@
 import { Action } from 'app/core/actions/location';
 import { Action } from 'app/core/actions/location';
 import { LocationState } from 'app/types';
 import { LocationState } from 'app/types';
 import { renderUrl } from 'app/core/utils/url';
 import { renderUrl } from 'app/core/utils/url';
+import _ from 'lodash';
 
 
 export const initialState: LocationState = {
 export const initialState: LocationState = {
   url: '',
   url: '',
@@ -12,11 +13,17 @@ export const initialState: LocationState = {
 export const locationReducer = (state = initialState, action: Action): LocationState => {
 export const locationReducer = (state = initialState, action: Action): LocationState => {
   switch (action.type) {
   switch (action.type) {
     case 'UPDATE_LOCATION': {
     case 'UPDATE_LOCATION': {
-      const { path, query, routeParams } = action.payload;
+      const { path, routeParams } = action.payload;
+      let query = action.payload.query || state.query;
+
+      if (action.payload.partial) {
+        query = _.defaults(query, state.query);
+      }
+
       return {
       return {
         url: renderUrl(path || state.path, query),
         url: renderUrl(path || state.path, query),
         path: path || state.path,
         path: path || state.path,
-        query: query || state.query,
+        query: query,
         routeParams: routeParams || state.routeParams,
         routeParams: routeParams || state.routeParams,
       };
       };
     }
     }

+ 14 - 20
public/app/core/services/dynamic_directive_srv.ts

@@ -3,7 +3,7 @@ import coreModule from '../core_module';
 
 
 class DynamicDirectiveSrv {
 class DynamicDirectiveSrv {
   /** @ngInject */
   /** @ngInject */
-  constructor(private $compile, private $rootScope) {}
+  constructor(private $compile) {}
 
 
   addDirective(element, name, scope) {
   addDirective(element, name, scope) {
     const child = angular.element(document.createElement(name));
     const child = angular.element(document.createElement(name));
@@ -14,25 +14,19 @@ class DynamicDirectiveSrv {
   }
   }
 
 
   link(scope, elem, attrs, options) {
   link(scope, elem, attrs, options) {
-    options
-      .directive(scope)
-      .then(directiveInfo => {
-        if (!directiveInfo || !directiveInfo.fn) {
-          elem.empty();
-          return;
-        }
-
-        if (!directiveInfo.fn.registered) {
-          coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
-          directiveInfo.fn.registered = true;
-        }
-
-        this.addDirective(elem, directiveInfo.name, scope);
-      })
-      .catch(err => {
-        console.log('Plugin load:', err);
-        this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
-      });
+    const directiveInfo = options.directive(scope);
+    if (!directiveInfo || !directiveInfo.fn) {
+      elem.empty();
+      return;
+    }
+
+    if (!directiveInfo.fn.registered) {
+      console.log('register panel tab');
+      coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
+      directiveInfo.fn.registered = true;
+    }
+
+    this.addDirective(elem, directiveInfo.name, scope);
   }
   }
 
 
   create(options) {
   create(options) {

+ 2 - 2
public/app/core/services/keybindingSrv.ts

@@ -148,7 +148,7 @@ export class KeybindingSrv {
     this.bind('mod+o', () => {
     this.bind('mod+o', () => {
       dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
       dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
       appEvents.emit('graph-hover-clear');
       appEvents.emit('graph-hover-clear');
-      this.$rootScope.$broadcast('refresh');
+      dashboard.startRefresh();
     });
     });
 
 
     this.bind('mod+s', e => {
     this.bind('mod+s', e => {
@@ -257,7 +257,7 @@ export class KeybindingSrv {
     });
     });
 
 
     this.bind('d r', () => {
     this.bind('d r', () => {
-      this.$rootScope.$broadcast('refresh');
+      dashboard.startRefresh();
     });
     });
 
 
     this.bind('d s', () => {
     this.bind('d s', () => {

+ 0 - 1
public/app/features/dashboard/all.ts

@@ -22,7 +22,6 @@ import './export_data/export_data_modal';
 import './ad_hoc_filters';
 import './ad_hoc_filters';
 import './repeat_option/repeat_option';
 import './repeat_option/repeat_option';
 import './dashgrid/DashboardGridDirective';
 import './dashgrid/DashboardGridDirective';
-import './dashgrid/PanelLoader';
 import './dashgrid/RowOptions';
 import './dashgrid/RowOptions';
 import './folder_picker/folder_picker';
 import './folder_picker/folder_picker';
 import './move_to_folder_modal/move_to_folder';
 import './move_to_folder_modal/move_to_folder';

+ 9 - 12
public/app/features/dashboard/dashboard_ctrl.ts

@@ -1,11 +1,10 @@
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
-import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
 import { PanelModel } from './panel_model';
 
 
-export class DashboardCtrl implements PanelContainer {
+export class DashboardCtrl {
   dashboard: DashboardModel;
   dashboard: DashboardModel;
   dashboardViewState: any;
   dashboardViewState: any;
   loadedFallbackDashboard: boolean;
   loadedFallbackDashboard: boolean;
@@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer {
     private dashboardSrv,
     private dashboardSrv,
     private unsavedChangesSrv,
     private unsavedChangesSrv,
     private dashboardViewStateSrv,
     private dashboardViewStateSrv,
-    public playlistSrv,
-    private panelLoader
+    public playlistSrv
   ) {
   ) {
     // temp hack due to way dashboards are loaded
     // temp hack due to way dashboards are loaded
     // can't use controllerAs on route yet
     // can't use controllerAs on route yet
@@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer {
     return this.dashboard;
     return this.dashboard;
   }
   }
 
 
-  getPanelLoader() {
-    return this.panelLoader;
-  }
-
-  timezoneChanged() {
-    this.$rootScope.$broadcast('refresh');
-  }
-
   getPanelContainer() {
   getPanelContainer() {
     return this;
     return this;
   }
   }
@@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer {
     this.dashboard.removePanel(panel);
     this.dashboard.removePanel(panel);
   }
   }
 
 
+  onDestroy() {
+    if (this.dashboard) {
+      this.dashboard.destroy();
+    }
+  }
+
   init(dashboard) {
   init(dashboard) {
     this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
     this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
     this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
     this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
     this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
     this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
+    this.$scope.$on('$destroy', this.onDestroy.bind(this));
     this.setupDashboard(dashboard);
     this.setupDashboard(dashboard);
   }
   }
 }
 }

+ 37 - 0
public/app/features/dashboard/dashboard_model.ts

@@ -200,6 +200,43 @@ export class DashboardModel {
     this.events.emit('view-mode-changed', panel);
     this.events.emit('view-mode-changed', panel);
   }
   }
 
 
+  timeRangeUpdated() {
+    this.events.emit('time-range-updated');
+  }
+
+  startRefresh() {
+    this.events.emit('refresh');
+
+    for (const panel of this.panels) {
+      if (!this.otherPanelInFullscreen(panel)) {
+        panel.refresh();
+      }
+    }
+  }
+
+  render() {
+    this.events.emit('render');
+
+    for (const panel of this.panels) {
+      panel.render();
+    }
+  }
+
+  panelInitialized(panel: PanelModel) {
+    if (!this.otherPanelInFullscreen(panel)) {
+      panel.refresh();
+    }
+  }
+
+  otherPanelInFullscreen(panel: PanelModel) {
+    return this.meta.fullscreen && !panel.fullscreen;
+  }
+
+  changePanelType(panel: PanelModel, pluginId: string) {
+    panel.changeType(pluginId);
+    this.events.emit('panel-type-changed', panel);
+  }
+
   private ensureListExist(data) {
   private ensureListExist(data) {
     if (!data) {
     if (!data) {
       data = {};
       data = {};

+ 4 - 7
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -3,7 +3,7 @@ import _ from 'lodash';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
-import { PanelContainer } from './PanelContainer';
+import { DashboardModel } from '../dashboard_model';
 import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
 import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
 import store from 'app/core/store';
 import store from 'app/core/store';
 import { LS_PANEL_COPY_KEY } from 'app/core/constants';
 import { LS_PANEL_COPY_KEY } from 'app/core/constants';
@@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words';
 
 
 export interface AddPanelPanelProps {
 export interface AddPanelPanelProps {
   panel: PanelModel;
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 }
 
 
 export interface AddPanelPanelState {
 export interface AddPanelPanelState {
@@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
   }
   }
 
 
   onAddPanel = panelPluginInfo => {
   onAddPanel = panelPluginInfo => {
-    const panelContainer = this.props.getPanelContainer();
-    const dashboard = panelContainer.getDashboard();
+    const dashboard = this.props.dashboard;
     const { gridPos } = this.props.panel;
     const { gridPos } = this.props.panel;
 
 
     const newPanel: any = {
     const newPanel: any = {
@@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
 
 
   handleCloseAddPanel(evt) {
   handleCloseAddPanel(evt) {
     evt.preventDefault();
     evt.preventDefault();
-    const panelContainer = this.props.getPanelContainer();
-    const dashboard = panelContainer.getDashboard();
-    dashboard.removePanel(dashboard.panels[0]);
+    this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
   }
   }
 
 
   renderText(text: string) {
   renderText(text: string) {

+ 22 - 24
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -3,7 +3,6 @@ import ReactGridLayout from 'react-grid-layout';
 import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
 import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
 import { DashboardPanel } from './DashboardPanel';
 import { DashboardPanel } from './DashboardPanel';
 import { DashboardModel } from '../dashboard_model';
 import { DashboardModel } from '../dashboard_model';
-import { PanelContainer } from './PanelContainer';
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import sizeMe from 'react-sizeme';
 import sizeMe from 'react-sizeme';
@@ -60,18 +59,15 @@ function GridWrapper({
 const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
 const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
 
 
 export interface DashboardGridProps {
 export interface DashboardGridProps {
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 }
 
 
 export class DashboardGrid extends React.Component<DashboardGridProps, any> {
 export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   gridToPanelMap: any;
   gridToPanelMap: any;
-  panelContainer: PanelContainer;
-  dashboard: DashboardModel;
   panelMap: { [id: string]: PanelModel };
   panelMap: { [id: string]: PanelModel };
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
-    this.panelContainer = this.props.getPanelContainer();
     this.onLayoutChange = this.onLayoutChange.bind(this);
     this.onLayoutChange = this.onLayoutChange.bind(this);
     this.onResize = this.onResize.bind(this);
     this.onResize = this.onResize.bind(this);
     this.onResizeStop = this.onResizeStop.bind(this);
     this.onResizeStop = this.onResizeStop.bind(this);
@@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
     this.state = { animated: false };
     this.state = { animated: false };
 
 
     // subscribe to dashboard events
     // subscribe to dashboard events
-    this.dashboard = this.panelContainer.getDashboard();
-    this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
+    const dashboard = this.props.dashboard;
+    dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
+    dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
+    dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
+    dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
+    dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
+    dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
+    dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
   }
   }
 
 
   buildLayout() {
   buildLayout() {
     const layout = [];
     const layout = [];
     this.panelMap = {};
     this.panelMap = {};
 
 
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       const stringId = panel.id.toString();
       const stringId = panel.id.toString();
       this.panelMap[stringId] = panel;
       this.panelMap[stringId] = panel;
 
 
@@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
       this.panelMap[newPos.i].updateGridPos(newPos);
       this.panelMap[newPos.i].updateGridPos(newPos);
     }
     }
 
 
-    this.dashboard.sortPanelsByGridPos();
+    this.props.dashboard.sortPanelsByGridPos();
   }
   }
 
 
   triggerForceUpdate() {
   triggerForceUpdate() {
@@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   }
   }
 
 
   onWidthChange() {
   onWidthChange() {
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       panel.resizeDone();
       panel.resizeDone();
     }
     }
   }
   }
 
 
+  onViewModeChanged(payload) {
+    this.setState({ animated: !payload.fullscreen });
+  }
+
   updateGridPos(item, layout) {
   updateGridPos(item, layout) {
     this.panelMap[item.i].updateGridPos(item);
     this.panelMap[item.i].updateGridPos(item);
 
 
@@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
 
 
   componentDidMount() {
   componentDidMount() {
     setTimeout(() => {
     setTimeout(() => {
-      this.setState(() => {
-        return { animated: true };
-      });
+      this.setState({ animated: true });
     });
     });
   }
   }
 
 
   renderPanels() {
   renderPanels() {
     const panelElements = [];
     const panelElements = [];
 
 
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
       const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
       panelElements.push(
       panelElements.push(
-        /** panel-id is set for html bookmarks */
-        <div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id.toString()}`}>
-          <DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
+        <div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
+          <DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
         </div>
         </div>
       );
       );
     }
     }
@@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
       <SizedReactLayoutGrid
       <SizedReactLayoutGrid
         className={classNames({ layout: true, animated: this.state.animated })}
         className={classNames({ layout: true, animated: this.state.animated })}
         layout={this.buildLayout()}
         layout={this.buildLayout()}
-        isResizable={this.dashboard.meta.canEdit}
-        isDraggable={this.dashboard.meta.canEdit}
+        isResizable={this.props.dashboard.meta.canEdit}
+        isDraggable={this.props.dashboard.meta.canEdit}
         onLayoutChange={this.onLayoutChange}
         onLayoutChange={this.onLayoutChange}
         onWidthChange={this.onWidthChange}
         onWidthChange={this.onWidthChange}
         onDragStop={this.onDragStop}
         onDragStop={this.onDragStop}

+ 1 - 3
public/app/features/dashboard/dashgrid/DashboardGridDirective.ts

@@ -1,6 +1,4 @@
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 import { DashboardGrid } from './DashboardGrid';
 import { DashboardGrid } from './DashboardGrid';
 
 
-react2AngularDirective('dashboardGrid', DashboardGrid, [
-  ['getPanelContainer', { watchDepth: 'reference', wrapApply: false }],
-]);
+react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);

+ 134 - 27
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -1,54 +1,161 @@
 import React from 'react';
 import React from 'react';
-import {PanelModel} from '../panel_model';
-import {PanelContainer} from './PanelContainer';
-import {AttachedPanel} from './PanelLoader';
-import {DashboardRow} from './DashboardRow';
-import {AddPanelPanel} from './AddPanelPanel';
+import config from 'app/core/config';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import { DashboardRow } from './DashboardRow';
+import { AddPanelPanel } from './AddPanelPanel';
+import { importPluginModule } from 'app/features/plugins/plugin_loader';
+import { PluginExports, PanelPlugin } from 'app/types/plugins';
+import { PanelChrome } from './PanelChrome';
+import { PanelEditor } from './PanelEditor';
 
 
-export interface DashboardPanelProps {
+export interface Props {
+  panelType: string;
   panel: PanelModel;
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 }
 
 
-export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
+export interface State {
+  pluginExports: PluginExports;
+}
+
+export class DashboardPanel extends React.Component<Props, State> {
   element: any;
   element: any;
-  attachedPanel: AttachedPanel;
+  angularPanel: AngularComponent;
+  pluginInfo: any;
+  specialPanels = {};
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
-    this.state = {};
+
+    this.state = {
+      pluginExports: null,
+    };
+
+    this.specialPanels['row'] = this.renderRow.bind(this);
+    this.specialPanels['add-panel'] = this.renderAddPanel.bind(this);
+  }
+
+  isSpecial() {
+    return this.specialPanels[this.props.panel.type];
+  }
+
+  renderRow() {
+    return <DashboardRow panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  renderAddPanel() {
+    return <AddPanelPanel panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  onPluginTypeChanged = (plugin: PanelPlugin) => {
+    this.props.panel.changeType(plugin.id);
+    this.loadPlugin();
+  };
+
+  onAngularPluginTypeChanged = () => {
+    this.loadPlugin();
+  };
+
+  loadPlugin() {
+    if (this.isSpecial()) {
+      return;
+    }
+
+    // handle plugin loading & changing of plugin type
+    if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) {
+      this.pluginInfo = config.panels[this.props.panel.type];
+
+      if (this.pluginInfo.exports) {
+        this.cleanUpAngularPanel();
+        this.setState({ pluginExports: this.pluginInfo.exports });
+      } else {
+        importPluginModule(this.pluginInfo.module).then(pluginExports => {
+          this.cleanUpAngularPanel();
+          // cache plugin exports (saves a promise async cycle next time)
+          this.pluginInfo.exports = pluginExports;
+          // update panel state
+          this.setState({ pluginExports: pluginExports });
+        });
+      }
+    }
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
-    if (!this.element) {
+    this.loadPlugin();
+  }
+
+  componentDidUpdate() {
+    this.loadPlugin();
+
+    // handle angular plugin loading
+    if (!this.element || this.angularPanel) {
       return;
       return;
     }
     }
 
 
-    const panelContainer = this.props.getPanelContainer();
-    const dashboard = panelContainer.getDashboard();
-    const loader = panelContainer.getPanelLoader();
-    this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
+    const loader = getAngularLoader();
+    const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
+    const scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard };
+    this.angularPanel = loader.load(this.element, scopeProps, template);
   }
   }
 
 
-  componentWillUnmount() {
-    if (this.attachedPanel) {
-      this.attachedPanel.destroy();
+  cleanUpAngularPanel() {
+    if (this.angularPanel) {
+      this.angularPanel.destroy();
+      this.angularPanel = null;
     }
     }
   }
   }
 
 
+  componentWillUnmount() {
+    this.cleanUpAngularPanel();
+  }
+
+  renderReactPanel() {
+    const { pluginExports } = this.state;
+    const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper';
+    const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
+
+    // this might look strange with these classes that change when edit, but
+    // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel
+    return (
+      <div className={containerClass}>
+        <div className={panelWrapperClass}>
+          <PanelChrome
+            component={pluginExports.PanelComponent}
+            panel={this.props.panel}
+            dashboard={this.props.dashboard}
+          />
+        </div>
+        {this.props.panel.isEditing && (
+          <div className="panel-editor-container__editor">
+            <PanelEditor
+              panel={this.props.panel}
+              panelType={this.props.panel.type}
+              dashboard={this.props.dashboard}
+              onTypeChanged={this.onPluginTypeChanged}
+              pluginExports={pluginExports}
+            />
+          </div>
+        )}
+      </div>
+    );
+  }
+
   render() {
   render() {
-    // special handling for rows
-    if (this.props.panel.type === 'row') {
-      return <DashboardRow panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
+    if (this.isSpecial()) {
+      return this.specialPanels[this.props.panel.type]();
     }
     }
 
 
-    if (this.props.panel.type === 'add-panel') {
-      return <AddPanelPanel panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
+    if (!this.state.pluginExports) {
+      return null;
     }
     }
 
 
-    return (
-      <div ref={element => this.element = element} className="panel-height-helper" />
-    );
+    if (this.state.pluginExports.PanelComponent) {
+      return this.renderReactPanel();
+    }
+
+    // legacy angular rendering
+    return <div ref={element => (this.element = element)} className="panel-height-helper" />;
   }
   }
 }
 }
-

+ 7 - 17
public/app/features/dashboard/dashgrid/DashboardRow.tsx

@@ -1,19 +1,16 @@
 import React from 'react';
 import React from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
-import { PanelContainer } from './PanelContainer';
+import { DashboardModel } from '../dashboard_model';
 import templateSrv from 'app/features/templating/template_srv';
 import templateSrv from 'app/features/templating/template_srv';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 
 
 export interface DashboardRowProps {
 export interface DashboardRowProps {
   panel: PanelModel;
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 }
 
 
 export class DashboardRow extends React.Component<DashboardRowProps, any> {
 export class DashboardRow extends React.Component<DashboardRowProps, any> {
-  dashboard: any;
-  panelContainer: any;
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -21,9 +18,6 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       collapsed: this.props.panel.collapsed,
       collapsed: this.props.panel.collapsed,
     };
     };
 
 
-    this.panelContainer = this.props.getPanelContainer();
-    this.dashboard = this.panelContainer.getDashboard();
-
     this.toggle = this.toggle.bind(this);
     this.toggle = this.toggle.bind(this);
     this.openSettings = this.openSettings.bind(this);
     this.openSettings = this.openSettings.bind(this);
     this.delete = this.delete.bind(this);
     this.delete = this.delete.bind(this);
@@ -31,7 +25,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
   }
 
 
   toggle() {
   toggle() {
-    this.dashboard.toggleRow(this.props.panel);
+    this.props.dashboard.toggleRow(this.props.panel);
 
 
     this.setState(prevState => {
     this.setState(prevState => {
       return { collapsed: !prevState.collapsed };
       return { collapsed: !prevState.collapsed };
@@ -39,7 +33,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
   }
 
 
   update() {
   update() {
-    this.dashboard.processRepeats();
+    this.props.dashboard.processRepeats();
     this.forceUpdate();
     this.forceUpdate();
   }
   }
 
 
@@ -61,14 +55,10 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       altActionText: 'Delete row only',
       altActionText: 'Delete row only',
       icon: 'fa-trash',
       icon: 'fa-trash',
       onConfirm: () => {
       onConfirm: () => {
-        const panelContainer = this.props.getPanelContainer();
-        const dashboard = panelContainer.getDashboard();
-        dashboard.removeRow(this.props.panel, true);
+        this.props.dashboard.removeRow(this.props.panel, true);
       },
       },
       onAltAction: () => {
       onAltAction: () => {
-        const panelContainer = this.props.getPanelContainer();
-        const dashboard = panelContainer.getDashboard();
-        dashboard.removeRow(this.props.panel, false);
+        this.props.dashboard.removeRow(this.props.panel, false);
       },
       },
     });
     });
   }
   }
@@ -87,7 +77,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
     const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
     const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
     const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
     const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
     const panels = count === 1 ? 'panel' : 'panels';
     const panels = count === 1 ? 'panel' : 'panels';
-    const canEdit = this.dashboard.meta.canEdit === true;
+    const canEdit = this.props.dashboard.meta.canEdit === true;
 
 
     return (
     return (
       <div className={classes}>
       <div className={classes}>

+ 151 - 0
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -0,0 +1,151 @@
+// Library
+import React, { Component } from 'react';
+
+// Services
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+
+// Types
+import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types';
+
+interface RenderProps {
+  loading: LoadingState;
+  timeSeries: TimeSeries[];
+}
+
+export interface Props {
+  datasource: string | null;
+  queries: any[];
+  panelId?: number;
+  dashboardId?: number;
+  isVisible?: boolean;
+  timeRange?: TimeRange;
+  refreshCounter: number;
+  children: (r: RenderProps) => JSX.Element;
+}
+
+export interface State {
+  isFirstLoad: boolean;
+  loading: LoadingState;
+  response: DataQueryResponse;
+}
+
+export class DataPanel extends Component<Props, State> {
+  static defaultProps = {
+    isVisible: true,
+    panelId: 1,
+    dashboardId: 1,
+  };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      loading: LoadingState.NotStarted,
+      response: {
+        data: [],
+      },
+      isFirstLoad: true,
+    };
+  }
+
+  componentDidMount() {
+    console.log('DataPanel mount');
+  }
+
+  async componentDidUpdate(prevProps: Props) {
+    if (!this.hasPropsChanged(prevProps)) {
+      return;
+    }
+
+    this.issueQueries();
+  }
+
+  hasPropsChanged(prevProps: Props) {
+    return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible;
+  }
+
+  issueQueries = async () => {
+    const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props;
+
+    if (!isVisible) {
+      return;
+    }
+
+    if (!queries.length) {
+      this.setState({ loading: LoadingState.Done });
+      return;
+    }
+
+    this.setState({ loading: LoadingState.Loading });
+
+    try {
+      const dataSourceSrv = getDatasourceSrv();
+      const ds = await dataSourceSrv.get(datasource);
+
+      const queryOptions: DataQueryOptions = {
+        timezone: 'browser',
+        panelId: panelId,
+        dashboardId: dashboardId,
+        range: timeRange,
+        rangeRaw: timeRange.raw,
+        interval: '1s',
+        intervalMs: 60000,
+        targets: queries,
+        maxDataPoints: 500,
+        scopedVars: {},
+        cacheTimeout: null,
+      };
+
+      console.log('Issuing DataPanel query', queryOptions);
+      const resp = await ds.query(queryOptions);
+      console.log('Issuing DataPanel query Resp', resp);
+
+      this.setState({
+        loading: LoadingState.Done,
+        response: resp,
+        isFirstLoad: false,
+      });
+    } catch (err) {
+      console.log('Loading error', err);
+      this.setState({ loading: LoadingState.Error, isFirstLoad: false });
+    }
+  };
+
+  render() {
+    const { response, loading, isFirstLoad } = this.state;
+    console.log('data panel render');
+    const timeSeries = response.data;
+
+    if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
+      return (
+        <div className="loading">
+          <p>Loading</p>
+        </div>
+      );
+    }
+
+    return (
+      <>
+        {this.loadingSpinner}
+        {this.props.children({
+          timeSeries,
+          loading,
+        })}
+      </>
+    );
+  }
+
+  private get loadingSpinner(): JSX.Element {
+    const { loading } = this.state;
+
+    if (loading === LoadingState.Loading) {
+      return (
+        <div className="panel__loading">
+          <i className="fa fa-spinner fa-spin" />
+        </div>
+      );
+    }
+
+    return null;
+  }
+}

+ 84 - 0
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -0,0 +1,84 @@
+// Libraries
+import React, { ComponentClass, PureComponent } from 'react';
+
+// Services
+import { getTimeSrv } from '../time_srv';
+
+// Components
+import { PanelHeader } from './PanelHeader';
+import { DataPanel } from './DataPanel';
+
+// Types
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { TimeRange, PanelProps } from 'app/types';
+
+export interface Props {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+  component: ComponentClass<PanelProps>;
+}
+
+export interface State {
+  refreshCounter: number;
+  timeRange?: TimeRange;
+}
+
+export class PanelChrome extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      refreshCounter: 0,
+    };
+  }
+
+  componentDidMount() {
+    this.props.panel.events.on('refresh', this.onRefresh);
+    this.props.dashboard.panelInitialized(this.props.panel);
+  }
+
+  componentWillUnmount() {
+    this.props.panel.events.off('refresh', this.onRefresh);
+  }
+
+  onRefresh = () => {
+    const timeSrv = getTimeSrv();
+    const timeRange = timeSrv.timeRange();
+
+    this.setState({
+      refreshCounter: this.state.refreshCounter + 1,
+      timeRange: timeRange,
+    });
+  };
+
+  get isVisible() {
+    return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
+  }
+
+  render() {
+    const { panel, dashboard } = this.props;
+    const { datasource, targets } = panel;
+    const { refreshCounter, timeRange } = this.state;
+    const PanelComponent = this.props.component;
+
+    return (
+      <div className="panel-container">
+        <PanelHeader panel={panel} dashboard={dashboard} />
+        <div className="panel-content">
+          <DataPanel
+            datasource={datasource}
+            queries={targets}
+            timeRange={timeRange}
+            isVisible={this.isVisible}
+            refreshCounter={refreshCounter}
+          >
+            {({ loading, timeSeries }) => {
+              return <PanelComponent loading={loading} timeSeries={timeSeries} timeRange={timeRange} />;
+            }}
+          </DataPanel>
+        </div>
+      </div>
+    );
+  }
+}

+ 0 - 7
public/app/features/dashboard/dashgrid/PanelContainer.ts

@@ -1,7 +0,0 @@
-import { DashboardModel } from '../dashboard_model';
-import { PanelLoader } from './PanelLoader';
-
-export interface PanelContainer {
-  getPanelLoader(): PanelLoader;
-  getDashboard(): DashboardModel;
-}

+ 121 - 0
public/app/features/dashboard/dashgrid/PanelEditor.tsx

@@ -0,0 +1,121 @@
+import React from 'react';
+import classNames from 'classnames';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { store } from 'app/store/configureStore';
+import { QueriesTab } from './QueriesTab';
+import { PanelPlugin, PluginExports } from 'app/types/plugins';
+import { VizTypePicker } from './VizTypePicker';
+import { updateLocation } from 'app/core/actions';
+
+interface PanelEditorProps {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+  panelType: string;
+  pluginExports: PluginExports;
+  onTypeChanged: (newType: PanelPlugin) => void;
+}
+
+interface PanelEditorTab {
+  id: string;
+  text: string;
+  icon: string;
+}
+
+export class PanelEditor extends React.Component<PanelEditorProps, any> {
+  tabs: PanelEditorTab[];
+
+  constructor(props) {
+    super(props);
+
+    this.tabs = [
+      { id: 'queries', text: 'Queries', icon: 'fa fa-database' },
+      { id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' },
+    ];
+  }
+
+  renderQueriesTab() {
+    return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  renderPanelOptions() {
+    const { pluginExports } = this.props;
+
+    if (pluginExports.PanelOptions) {
+      const PanelOptions = pluginExports.PanelOptions;
+      return <PanelOptions />;
+    } else {
+      return <p>Visualization has no options</p>;
+    }
+  }
+
+  renderVizTab() {
+    return (
+      <div className="viz-editor">
+        <div className="viz-editor-col1">
+          <VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.props.onTypeChanged} />
+        </div>
+        <div className="viz-editor-col2">
+          <h5 className="page-heading">Options</h5>
+          {this.renderPanelOptions()}
+        </div>
+      </div>
+    );
+  }
+
+  onChangeTab = (tab: PanelEditorTab) => {
+    store.dispatch(
+      updateLocation({
+        query: { tab: tab.id },
+        partial: true,
+      })
+    );
+  };
+
+  render() {
+    const { location } = store.getState();
+    const activeTab = location.query.tab || 'queries';
+
+    return (
+      <div className="tabbed-view tabbed-view--new">
+        <div className="tabbed-view-header">
+          <ul className="gf-tabs">
+            {this.tabs.map(tab => {
+              return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
+            })}
+          </ul>
+
+          <button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
+            <i className="fa fa-remove" />
+          </button>
+        </div>
+
+        <div className="tabbed-view-body">
+          {activeTab === 'queries' && this.renderQueriesTab()}
+          {activeTab === 'visualization' && this.renderVizTab()}
+        </div>
+      </div>
+    );
+  }
+}
+
+interface TabItemParams {
+  tab: PanelEditorTab;
+  activeTab: string;
+  onClick: (tab: PanelEditorTab) => void;
+}
+
+function TabItem({ tab, activeTab, onClick }: TabItemParams) {
+  const tabClasses = classNames({
+    'gf-tabs-link': true,
+    active: activeTab === tab.id,
+  });
+
+  return (
+    <li className="gf-tabs-item" key={tab.id}>
+      <a className={tabClasses} onClick={() => onClick(tab)}>
+        <i className={tab.icon} /> {tab.text}
+      </a>
+    </li>
+  );
+}

+ 83 - 0
public/app/features/dashboard/dashgrid/PanelHeader.tsx

@@ -0,0 +1,83 @@
+import React from 'react';
+import classNames from 'classnames';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { store } from 'app/store/configureStore';
+import { updateLocation } from 'app/core/actions';
+
+interface PanelHeaderProps {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+}
+
+export class PanelHeader extends React.Component<PanelHeaderProps, any> {
+  onEditPanel = () => {
+    store.dispatch(
+      updateLocation({
+        query: {
+          panelId: this.props.panel.id,
+          edit: true,
+          fullscreen: true,
+        },
+      })
+    );
+  };
+
+  onViewPanel = () => {
+    store.dispatch(
+      updateLocation({
+        query: {
+          panelId: this.props.panel.id,
+          edit: false,
+          fullscreen: true,
+        },
+      })
+    );
+  };
+
+  render() {
+    const isFullscreen = false;
+    const isLoading = false;
+    const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
+
+    return (
+      <div className={panelHeaderClass}>
+        <span className="panel-info-corner">
+          <i className="fa" />
+          <span className="panel-info-corner-inner" />
+        </span>
+
+        {isLoading && (
+          <span className="panel-loading">
+            <i className="fa fa-spinner fa-spin" />
+          </span>
+        )}
+
+        <div className="panel-title-container">
+          <span className="panel-title">
+            <span className="icon-gf panel-alert-icon" />
+            <span className="panel-title-text">{this.props.panel.title}</span>
+            <span className="panel-menu-container dropdown">
+              <span className="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown" />
+              <ul className="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
+                <li>
+                  <a onClick={this.onEditPanel}>
+                    <i className="fa fa-fw fa-edit" /> Edit
+                  </a>
+                </li>
+                <li>
+                  <a onClick={this.onViewPanel}>
+                    <i className="fa fa-fw fa-eye" /> View
+                  </a>
+                </li>
+              </ul>
+            </span>
+            <span className="panel-time-info">
+              <i className="fa fa-clock-o" /> 4m
+            </span>
+          </span>
+        </div>
+      </div>
+    );
+  }
+}

+ 53 - 0
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -0,0 +1,53 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Services & utils
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+
+// Types
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+
+interface Props {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+}
+
+export class QueriesTab extends PureComponent<Props> {
+  element: any;
+  component: AngularComponent;
+
+  constructor(props) {
+    super(props);
+  }
+
+  componentDidMount() {
+    if (!this.element) {
+      return;
+    }
+
+    const { panel, dashboard } = this.props;
+
+    const loader = getAngularLoader();
+    const template = '<metrics-tab />';
+    const scopeProps = {
+      ctrl: {
+        panel: panel,
+        dashboard: dashboard,
+        refresh: () => panel.refresh(),
+      },
+    };
+
+    this.component = loader.load(this.element, scopeProps, template);
+  }
+
+  componentWillUnmount() {
+    if (this.component) {
+      this.component.destroy();
+    }
+  }
+
+  render() {
+    return <div ref={element => (this.element = element)} className="panel-height-helper" />;
+  }
+}

+ 69 - 0
public/app/features/dashboard/dashgrid/VizTypePicker.tsx

@@ -0,0 +1,69 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import config from 'app/core/config';
+import { PanelPlugin } from 'app/types/plugins';
+import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
+import _ from 'lodash';
+
+interface Props {
+  currentType: string;
+  onTypeChanged: (newType: PanelPlugin) => void;
+}
+
+interface State {
+  pluginList: PanelPlugin[];
+}
+
+export class VizTypePicker extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pluginList: this.getPanelPlugins(''),
+    };
+  }
+
+  getPanelPlugins(filter) {
+    const panels = _.chain(config.panels)
+      .filter({ hideFromList: false })
+      .map(item => item)
+      .value();
+
+    // add sort by sort property
+    return _.sortBy(panels, 'sort');
+  }
+
+  renderVizPlugin = (plugin, index) => {
+    const cssClass = classNames({
+      'viz-picker__item': true,
+      'viz-picker__item--selected': plugin.id === this.props.currentType,
+    });
+
+    return (
+      <div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
+        <img className="viz-picker__item-img" src={plugin.info.logos.small} />
+        <div className="viz-picker__item-name">{plugin.name}</div>
+      </div>
+    );
+  };
+
+  render() {
+    return (
+      <div className="viz-picker">
+        <div className="viz-picker__search">
+          <div className="gf-form gf-form--grow">
+            <label className="gf-form--has-input-icon gf-form--grow">
+              <input type="text" className="gf-form-input" placeholder="Search type" />
+              <i className="gf-form-input-icon fa fa-search" />
+            </label>
+          </div>
+        </div>
+        <div className="viz-picker__items">
+          <CustomScrollbar>
+            <div className="scroll-margin-helper">{this.state.pluginList.map(this.renderVizPlugin)}</div>
+          </CustomScrollbar>
+        </div>
+      </div>
+    );
+  }
+}

+ 2 - 0
public/app/features/dashboard/dashnav/dashnav.ts

@@ -42,6 +42,8 @@ export class DashNavCtrl {
     } else if (search.fullscreen) {
     } else if (search.fullscreen) {
       delete search.fullscreen;
       delete search.fullscreen;
       delete search.edit;
       delete search.edit;
+      delete search.tab;
+      delete search.panelId;
     }
     }
     this.$location.search(search);
     this.$location.search(search);
   }
   }

+ 45 - 4
public/app/features/dashboard/panel_model.ts

@@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = {
   events: true,
   events: true,
   fullscreen: true,
   fullscreen: true,
   isEditing: true,
   isEditing: true,
+  hasRefreshed: true,
+};
+
+const defaults: any = {
+  gridPos: { x: 0, y: 0, h: 3, w: 6 },
+  datasource: null,
+  targets: [{}],
 };
 };
 
 
 export class PanelModel {
 export class PanelModel {
@@ -31,10 +38,14 @@ export class PanelModel {
   collapsed?: boolean;
   collapsed?: boolean;
   panels?: any;
   panels?: any;
   soloMode?: boolean;
   soloMode?: boolean;
+  targets: any[];
+  datasource: string;
+  thresholds?: any;
 
 
   // non persisted
   // non persisted
   fullscreen: boolean;
   fullscreen: boolean;
   isEditing: boolean;
   isEditing: boolean;
+  hasRefreshed: boolean;
   events: Emitter;
   events: Emitter;
 
 
   constructor(model) {
   constructor(model) {
@@ -45,9 +56,8 @@ export class PanelModel {
       this[property] = model[property];
       this[property] = model[property];
     }
     }
 
 
-    if (!this.gridPos) {
-      this.gridPos = { x: 0, y: 0, h: 3, w: 6 };
-    }
+    // defaults
+    _.defaultsDeep(this, _.cloneDeep(defaults));
   }
   }
 
 
   getSaveModel() {
   getSaveModel() {
@@ -57,6 +67,10 @@ export class PanelModel {
         continue;
         continue;
       }
       }
 
 
+      if (_.isEqual(this[property], defaults[property])) {
+        continue;
+      }
+
       model[property] = _.cloneDeep(this[property]);
       model[property] = _.cloneDeep(this[property]);
     }
     }
 
 
@@ -82,7 +96,6 @@ export class PanelModel {
     this.gridPos.h = newPos.h;
     this.gridPos.h = newPos.h;
 
 
     if (sizeChanged) {
     if (sizeChanged) {
-      console.log('PanelModel sizeChanged event and render events fired');
       this.events.emit('panel-size-changed');
       this.events.emit('panel-size-changed');
     }
     }
   }
   }
@@ -91,6 +104,34 @@ export class PanelModel {
     this.events.emit('panel-size-changed');
     this.events.emit('panel-size-changed');
   }
   }
 
 
+  refresh() {
+    this.hasRefreshed = true;
+    this.events.emit('refresh');
+  }
+
+  render() {
+    if (!this.hasRefreshed) {
+      this.refresh();
+    } else {
+      this.events.emit('render');
+    }
+  }
+
+  panelInitialized() {
+    this.events.emit('panel-initialized');
+  }
+
+  initEditMode() {
+    this.events.emit('panel-init-edit-mode');
+  }
+
+  changeType(pluginId: string) {
+    this.type = pluginId;
+
+    delete this.thresholds;
+    delete this.alert;
+  }
+
   destroy() {
   destroy() {
     this.events.removeAllListeners();
     this.events.removeAllListeners();
   }
   }

+ 1 - 1
public/app/features/dashboard/settings/settings.ts

@@ -32,7 +32,7 @@ export class SettingsCtrl {
 
 
     this.$scope.$on('$destroy', () => {
     this.$scope.$on('$destroy', () => {
       this.dashboard.updateSubmenuVisibility();
       this.dashboard.updateSubmenuVisibility();
-      this.$rootScope.$broadcast('refresh');
+      this.dashboard.startRefresh();
       setTimeout(() => {
       setTimeout(() => {
         this.$rootScope.appEvent('dash-scroll', { restore: true });
         this.$rootScope.appEvent('dash-scroll', { restore: true });
       });
       });

+ 1 - 2
public/app/features/dashboard/share_snapshot_ctrl.ts

@@ -46,8 +46,7 @@ export class ShareSnapshotCtrl {
 
 
       $scope.loading = true;
       $scope.loading = true;
       $scope.snapshot.external = external;
       $scope.snapshot.external = external;
-
-      $rootScope.$broadcast('refresh');
+      $scope.dashboard.startRefresh();
 
 
       $timeout(() => {
       $timeout(() => {
         $scope.saveSnapshot(external);
         $scope.saveSnapshot(external);

+ 17 - 7
public/app/features/dashboard/specs/AddPanelPanel.test.tsx

@@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({
 }));
 }));
 
 
 describe('AddPanelPanel', () => {
 describe('AddPanelPanel', () => {
-  let wrapper, dashboardMock, getPanelContainer, panel;
+  let wrapper, dashboardMock, panel;
 
 
   beforeEach(() => {
   beforeEach(() => {
     config.panels = [
     config.panels = [
@@ -23,6 +23,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         hideFromList: false,
         name: 'Singlestat',
         name: 'Singlestat',
         sort: 2,
         sort: 2,
+        module: '',
+        baseUrl: '',
+        meta: {},
         info: {
         info: {
           logos: {
           logos: {
             small: '',
             small: '',
@@ -34,6 +37,9 @@ describe('AddPanelPanel', () => {
         hideFromList: true,
         hideFromList: true,
         name: 'Hidden',
         name: 'Hidden',
         sort: 100,
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
         info: {
           logos: {
           logos: {
             small: '',
             small: '',
@@ -45,6 +51,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         hideFromList: false,
         name: 'Graph',
         name: 'Graph',
         sort: 1,
         sort: 1,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
         info: {
           logos: {
           logos: {
             small: '',
             small: '',
@@ -56,6 +65,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         hideFromList: false,
         name: 'Zabbix',
         name: 'Zabbix',
         sort: 100,
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
         info: {
           logos: {
           logos: {
             small: '',
             small: '',
@@ -67,6 +79,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         hideFromList: false,
         name: 'Piechart',
         name: 'Piechart',
         sort: 100,
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
         info: {
           logos: {
           logos: {
             small: '',
             small: '',
@@ -77,13 +92,8 @@ describe('AddPanelPanel', () => {
 
 
     dashboardMock = { toggleRow: jest.fn() };
     dashboardMock = { toggleRow: jest.fn() };
 
 
-    getPanelContainer = jest.fn().mockReturnValue({
-      getDashboard: jest.fn().mockReturnValue(dashboardMock),
-      getPanelLoader: jest.fn(),
-    });
-
     panel = new PanelModel({ collapsed: false });
     panel = new PanelModel({ collapsed: false });
-    wrapper = shallow(<AddPanelPanel panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<AddPanelPanel panel={panel} dashboard={dashboardMock} />);
   });
   });
 
 
   it('should fetch all panels sorted with core plugins first', () => {
   it('should fetch all panels sorted with core plugins first', () => {

+ 4 - 9
public/app/features/dashboard/specs/DashboardRow.test.tsx

@@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow';
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 
 
 describe('DashboardRow', () => {
 describe('DashboardRow', () => {
-  let wrapper, panel, getPanelContainer, dashboardMock;
+  let wrapper, panel, dashboardMock;
 
 
   beforeEach(() => {
   beforeEach(() => {
     dashboardMock = {
     dashboardMock = {
@@ -14,13 +14,8 @@ describe('DashboardRow', () => {
       },
       },
     };
     };
 
 
-    getPanelContainer = jest.fn().mockReturnValue({
-      getDashboard: jest.fn().mockReturnValue(dashboardMock),
-      getPanelLoader: jest.fn(),
-    });
-
     panel = new PanelModel({ collapsed: false });
     panel = new PanelModel({ collapsed: false });
-    wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
   });
   });
 
 
   it('Should not have collapsed class when collaped is false', () => {
   it('Should not have collapsed class when collaped is false', () => {
@@ -41,14 +36,14 @@ describe('DashboardRow', () => {
 
 
   it('should not show row drag handle when cannot edit', () => {
   it('should not show row drag handle when cannot edit', () => {
     dashboardMock.meta.canEdit = false;
     dashboardMock.meta.canEdit = false;
-    wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
     expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
     expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
   });
   });
 
 
   it('should have zero actions when cannot edit', () => {
   it('should have zero actions when cannot edit', () => {
     dashboardMock.meta.canEdit = false;
     dashboardMock.meta.canEdit = false;
     panel = new PanelModel({ collapsed: false });
     panel = new PanelModel({ collapsed: false });
-    wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
     expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
     expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
   });
   });
 });
 });

+ 1 - 1
public/app/features/dashboard/specs/exporter.test.ts

@@ -240,5 +240,5 @@ stubs['-- Grafana --'] = {
 };
 };
 
 
 function getStub(arg) {
 function getStub(arg) {
-  return Promise.resolve(stubs[arg]);
+  return Promise.resolve(stubs[arg || 'gfdb']);
 }
 }

+ 7 - 7
public/app/features/dashboard/specs/viewstate_srv.test.ts

@@ -2,6 +2,7 @@
 import 'app/features/dashboard/view_state_srv';
 import 'app/features/dashboard/view_state_srv';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import { DashboardViewState } from '../view_state_srv';
 import { DashboardViewState } from '../view_state_srv';
+import { DashboardModel } from '../dashboard_model';
 
 
 describe('when updating view state', () => {
 describe('when updating view state', () => {
   const location = {
   const location = {
@@ -10,14 +11,13 @@ describe('when updating view state', () => {
   };
   };
 
 
   const $scope = {
   const $scope = {
+    appEvent: jest.fn(),
     onAppEvent: jest.fn(() => {}),
     onAppEvent: jest.fn(() => {}),
-    dashboard: {
-      meta: {},
-      panels: [],
-    },
+    dashboard: new DashboardModel({
+      panels: [{ id: 1 }],
+    }),
   };
   };
 
 
-  const $rootScope = {};
   let viewState;
   let viewState;
 
 
   beforeEach(() => {
   beforeEach(() => {
@@ -33,7 +33,7 @@ describe('when updating view state', () => {
       location.search = jest.fn(() => {
       location.search = jest.fn(() => {
         return { fullscreen: true, edit: true, panelId: 1 };
         return { fullscreen: true, edit: true, panelId: 1 };
       });
       });
-      viewState = new DashboardViewState($scope, location, {}, $rootScope);
+      viewState = new DashboardViewState($scope, location, {});
     });
     });
 
 
     it('should update querystring and view state', () => {
     it('should update querystring and view state', () => {
@@ -55,7 +55,7 @@ describe('when updating view state', () => {
 
 
   describe('to fullscreen false', () => {
   describe('to fullscreen false', () => {
     beforeEach(() => {
     beforeEach(() => {
-      viewState = new DashboardViewState($scope, location, {}, $rootScope);
+      viewState = new DashboardViewState($scope, location, {});
     });
     });
     it('should remove params from query string', () => {
     it('should remove params from query string', () => {
       viewState.update({ fullscreen: true, panelId: 1, edit: true });
       viewState.update({ fullscreen: true, panelId: 1, edit: true });

+ 2 - 2
public/app/features/dashboard/submenu/submenu.ts

@@ -7,13 +7,13 @@ export class SubmenuCtrl {
   dashboard: any;
   dashboard: any;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(private $rootScope, private variableSrv, private $location) {
+  constructor(private variableSrv, private $location) {
     this.annotations = this.dashboard.templating.list;
     this.annotations = this.dashboard.templating.list;
     this.variables = this.variableSrv.variables;
     this.variables = this.variableSrv.variables;
   }
   }
 
 
   annotationStateChanged() {
   annotationStateChanged() {
-    this.$rootScope.$broadcast('refresh');
+    this.dashboard.startRefresh();
   }
   }
 
 
   variableUpdated(variable) {
   variableUpdated(variable) {

+ 21 - 12
public/app/features/dashboard/time_srv.ts

@@ -1,8 +1,14 @@
+// Libraries
 import moment from 'moment';
 import moment from 'moment';
 import _ from 'lodash';
 import _ from 'lodash';
-import coreModule from 'app/core/core_module';
+
+// Utils
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
+import coreModule from 'app/core/core_module';
 import * as dateMath from 'app/core/utils/datemath';
 import * as dateMath from 'app/core/utils/datemath';
+// Types
+
+import { TimeRange } from 'app/types';
 
 
 export class TimeSrv {
 export class TimeSrv {
   time: any;
   time: any;
@@ -24,7 +30,6 @@ export class TimeSrv {
     document.addEventListener('visibilitychange', () => {
     document.addEventListener('visibilitychange', () => {
       if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
       if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
         this.autoRefreshBlocked = false;
         this.autoRefreshBlocked = false;
-
         this.refreshDashboard();
         this.refreshDashboard();
       }
       }
     });
     });
@@ -142,7 +147,7 @@ export class TimeSrv {
   }
   }
 
 
   refreshDashboard() {
   refreshDashboard() {
-    this.$rootScope.$broadcast('refresh');
+    this.dashboard.timeRangeUpdated();
   }
   }
 
 
   private startNextRefreshTimer(afterMs) {
   private startNextRefreshTimer(afterMs) {
@@ -201,7 +206,7 @@ export class TimeSrv {
     return range;
     return range;
   }
   }
 
 
-  timeRange() {
+  timeRange(): TimeRange {
     // make copies if they are moment  (do not want to return out internal moment, because they are mutable!)
     // make copies if they are moment  (do not want to return out internal moment, because they are mutable!)
     const raw = {
     const raw = {
       from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from,
       from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from,
@@ -223,17 +228,21 @@ export class TimeSrv {
     const timespan = range.to.valueOf() - range.from.valueOf();
     const timespan = range.to.valueOf() - range.from.valueOf();
     const center = range.to.valueOf() - timespan / 2;
     const center = range.to.valueOf() - timespan / 2;
 
 
-    let to = center + timespan * factor / 2;
-    let from = center - timespan * factor / 2;
-
-    if (to > Date.now() && range.to <= Date.now()) {
-      const offset = to - Date.now();
-      from = from - offset;
-      to = Date.now();
-    }
+    const to = center + timespan * factor / 2;
+    const from = center - timespan * factor / 2;
 
 
     this.setTime({ from: moment.utc(from), to: moment.utc(to) });
     this.setTime({ from: moment.utc(from), to: moment.utc(to) });
   }
   }
 }
 }
 
 
+let singleton;
+
+export function setTimeSrv(srv: TimeSrv) {
+  singleton = srv;
+}
+
+export function getTimeSrv(): TimeSrv {
+  return singleton;
+}
+
 coreModule.service('timeSrv', TimeSrv);
 coreModule.service('timeSrv', TimeSrv);

+ 1 - 1
public/app/features/dashboard/timepicker/settings.html

@@ -5,7 +5,7 @@
 		<div class="gf-form">
 		<div class="gf-form">
 			<label class="gf-form-label width-10">Timezone</label>
 			<label class="gf-form-label width-10">Timezone</label>
 			<div class="gf-form-select-wrapper">
 			<div class="gf-form-select-wrapper">
-				<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
+				<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]"></select>
 			</div>
 			</div>
 		</div>
 		</div>
 
 

+ 2 - 1
public/app/features/dashboard/timepicker/timepicker.ts

@@ -31,9 +31,10 @@ export class TimePickerCtrl {
 
 
     $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
     $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
     $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
     $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
-    $rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
     $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
     $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
 
 
+    this.dashboard.on('refresh', this.onRefresh.bind(this), $scope);
+
     // init options
     // init options
     this.panel = this.dashboard.timepicker;
     this.panel = this.dashboard.timepicker;
     _.defaults(this.panel, TimePickerCtrl.defaults);
     _.defaults(this.panel, TimePickerCtrl.defaults);

+ 27 - 85
public/app/features/dashboard/view_state_srv.ts

@@ -1,6 +1,7 @@
 import angular from 'angular';
 import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import config from 'app/core/config';
 import config from 'app/core/config';
+import appEvents from 'app/core/app_events';
 import { DashboardModel } from './dashboard_model';
 import { DashboardModel } from './dashboard_model';
 
 
 // represents the transient view state
 // represents the transient view state
@@ -10,12 +11,11 @@ export class DashboardViewState {
   panelScopes: any;
   panelScopes: any;
   $scope: any;
   $scope: any;
   dashboard: DashboardModel;
   dashboard: DashboardModel;
-  editStateChanged: any;
   fullscreenPanel: any;
   fullscreenPanel: any;
   oldTimeRange: any;
   oldTimeRange: any;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor($scope, private $location, private $timeout, private $rootScope) {
+  constructor($scope, private $location, private $timeout) {
     const self = this;
     const self = this;
     self.state = {};
     self.state = {};
     self.panelScopes = [];
     self.panelScopes = [];
@@ -33,10 +33,6 @@ export class DashboardViewState {
       self.update(payload);
       self.update(payload);
     });
     });
 
 
-    $scope.onAppEvent('panel-initialized', (evt, payload) => {
-      self.registerPanel(payload.scope);
-    });
-
     // this marks changes to location during this digest cycle as not to add history item
     // this marks changes to location during this digest cycle as not to add history item
     // don't want url changes like adding orgId to add browser history
     // don't want url changes like adding orgId to add browser history
     $location.replace();
     $location.replace();
@@ -75,9 +71,6 @@ export class DashboardViewState {
       }
       }
     }
     }
 
 
-    // remember if editStateChanged
-    this.editStateChanged = (state.edit || false) !== (this.state.edit || false);
-
     _.extend(this.state, state);
     _.extend(this.state, state);
     this.dashboard.meta.fullscreen = this.state.fullscreen;
     this.dashboard.meta.fullscreen = this.state.fullscreen;
 
 
@@ -124,110 +117,59 @@ export class DashboardViewState {
   }
   }
 
 
   syncState() {
   syncState() {
-    if (this.panelScopes.length === 0) {
-      return;
-    }
-
     if (this.dashboard.meta.fullscreen) {
     if (this.dashboard.meta.fullscreen) {
-      const panelScope = this.getPanelScope(this.state.panelId);
-      if (!panelScope) {
-        return;
-      }
+      const panel = this.dashboard.getPanelById(this.state.panelId);
 
 
-      if (this.fullscreenPanel) {
-        // if already fullscreen
-        if (this.fullscreenPanel === panelScope && this.editStateChanged === false) {
-          return;
-        } else {
-          this.leaveFullscreen(false);
-        }
-      }
-
-      if (!panelScope.ctrl.editModeInitiated) {
-        panelScope.ctrl.initEditMode();
+      if (!panel) {
+        return;
       }
       }
 
 
-      if (!panelScope.ctrl.fullscreen) {
-        this.enterFullscreen(panelScope);
+      if (!panel.fullscreen) {
+        this.enterFullscreen(panel);
+      } else {
+        // already in fullscreen view just update the view mode
+        this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
       }
       }
     } else if (this.fullscreenPanel) {
     } else if (this.fullscreenPanel) {
-      this.leaveFullscreen(true);
+      this.leaveFullscreen();
     }
     }
   }
   }
 
 
-  getPanelScope(id) {
-    return _.find(this.panelScopes, panelScope => {
-      return panelScope.ctrl.panel.id === id;
-    });
-  }
-
-  leaveFullscreen(render) {
-    const self = this;
-    const ctrl = self.fullscreenPanel.ctrl;
-
-    ctrl.editMode = false;
-    ctrl.fullscreen = false;
+  leaveFullscreen() {
+    const panel = this.fullscreenPanel;
 
 
-    this.dashboard.setViewMode(ctrl.panel, false, false);
-    this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id });
-    this.$scope.appEvent('dash-scroll', { restore: true });
+    this.dashboard.setViewMode(panel, false, false);
 
 
-    if (!render) {
-      return false;
-    }
+    delete this.fullscreenPanel;
 
 
     this.$timeout(() => {
     this.$timeout(() => {
-      if (self.oldTimeRange !== ctrl.range) {
-        self.$rootScope.$broadcast('refresh');
+      appEvents.emit('dash-scroll', { restore: true });
+
+      if (this.oldTimeRange !== this.dashboard.time) {
+        this.dashboard.startRefresh();
       } else {
       } else {
-        self.$rootScope.$broadcast('render');
+        this.dashboard.render();
       }
       }
-      delete self.fullscreenPanel;
     });
     });
-    return true;
   }
   }
 
 
-  enterFullscreen(panelScope) {
-    const ctrl = panelScope.ctrl;
-
-    ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
-    ctrl.fullscreen = true;
+  enterFullscreen(panel) {
+    const isEditing = this.state.edit && this.dashboard.meta.canEdit;
 
 
-    this.oldTimeRange = ctrl.range;
-    this.fullscreenPanel = panelScope;
+    this.oldTimeRange = this.dashboard.time;
+    this.fullscreenPanel = panel;
 
 
     // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
     // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
     this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
     this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
-    this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
-    this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
-  }
-
-  registerPanel(panelScope) {
-    const self = this;
-    self.panelScopes.push(panelScope);
-
-    if (!self.dashboard.meta.soloMode) {
-      if (self.state.panelId === panelScope.ctrl.panel.id) {
-        if (self.state.edit) {
-          panelScope.ctrl.editPanel();
-        } else {
-          panelScope.ctrl.viewPanel();
-        }
-      }
-    }
-
-    const unbind = panelScope.$on('$destroy', () => {
-      self.panelScopes = _.without(self.panelScopes, panelScope);
-      unbind();
-    });
+    this.dashboard.setViewMode(panel, true, isEditing);
   }
   }
 }
 }
 
 
 /** @ngInject */
 /** @ngInject */
-export function dashboardViewStateSrv($location, $timeout, $rootScope) {
+export function dashboardViewStateSrv($location, $timeout) {
   return {
   return {
     create: $scope => {
     create: $scope => {
-      return new DashboardViewState($scope, $location, $timeout, $rootScope);
+      return new DashboardViewState($scope, $location, $timeout);
     },
     },
   };
   };
 }
 }

+ 1 - 28
public/app/features/panel/metrics_panel_ctrl.ts

@@ -7,13 +7,11 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as dateMath from 'app/core/utils/datemath';
 import * as dateMath from 'app/core/utils/datemath';
 import { getExploreUrl } from 'app/core/utils/explore';
 import { getExploreUrl } from 'app/core/utils/explore';
-
 import { metricsTabDirective } from './metrics_tab';
 import { metricsTabDirective } from './metrics_tab';
 
 
 class MetricsPanelCtrl extends PanelCtrl {
 class MetricsPanelCtrl extends PanelCtrl {
   scope: any;
   scope: any;
   datasource: any;
   datasource: any;
-  datasourceName: any;
   $q: any;
   $q: any;
   $timeout: any;
   $timeout: any;
   contextSrv: any;
   contextSrv: any;
@@ -45,10 +43,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.scope = $scope;
     this.scope = $scope;
     this.panel.datasource = this.panel.datasource || null;
     this.panel.datasource = this.panel.datasource || null;
 
 
-    if (!this.panel.targets) {
-      this.panel.targets = [{}];
-    }
-
     this.events.on('refresh', this.onMetricsPanelRefresh.bind(this));
     this.events.on('refresh', this.onMetricsPanelRefresh.bind(this));
     this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this));
     this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this));
     this.events.on('panel-teardown', this.onPanelTearDown.bind(this));
     this.events.on('panel-teardown', this.onPanelTearDown.bind(this));
@@ -62,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
   }
 
 
   private onInitMetricsPanelEditMode() {
   private onInitMetricsPanelEditMode() {
-    this.addEditorTab('Metrics', metricsTabDirective);
+    this.addEditorTab('Metrics', metricsTabDirective, 1, 'fa fa-database');
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
   }
   }
 
 
@@ -291,27 +285,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     });
     });
   }
   }
 
 
-  setDatasource(datasource) {
-    // switching to mixed
-    if (datasource.meta.mixed) {
-      _.each(this.panel.targets, target => {
-        target.datasource = this.panel.datasource;
-        if (!target.datasource) {
-          target.datasource = config.defaultDatasource;
-        }
-      });
-    } else if (this.datasource && this.datasource.meta.mixed) {
-      _.each(this.panel.targets, target => {
-        delete target.datasource;
-      });
-    }
-
-    this.panel.datasource = datasource.value;
-    this.datasourceName = datasource.name;
-    this.datasource = null;
-    this.refresh();
-  }
-
   getAdditionalMenuItems() {
   getAdditionalMenuItems() {
     const items = [];
     const items = [];
     if (
     if (

+ 34 - 2
public/app/features/panel/metrics_tab.ts

@@ -1,6 +1,14 @@
-import { DashboardModel } from '../dashboard/dashboard_model';
+// Libraries
+import _ from 'lodash';
 import Remarkable from 'remarkable';
 import Remarkable from 'remarkable';
 
 
+// Services & utils
+import coreModule from 'app/core/core_module';
+import config from 'app/core/config';
+
+// Types
+import { DashboardModel } from '../dashboard/dashboard_model';
+
 export class MetricsTabCtrl {
 export class MetricsTabCtrl {
   dsName: string;
   dsName: string;
   panel: any;
   panel: any;
@@ -24,6 +32,9 @@ export class MetricsTabCtrl {
     $scope.ctrl = this;
     $scope.ctrl = this;
 
 
     this.panel = this.panelCtrl.panel;
     this.panel = this.panelCtrl.panel;
+    this.panel.datasource = this.panel.datasource || null;
+    this.panel.targets = this.panel.targets || [{}];
+
     this.dashboard = this.panelCtrl.dashboard;
     this.dashboard = this.panelCtrl.dashboard;
     this.datasources = datasourceSrv.getMetricSources();
     this.datasources = datasourceSrv.getMetricSources();
     this.panelDsValue = this.panelCtrl.panel.datasource;
     this.panelDsValue = this.panelCtrl.panel.datasource;
@@ -66,10 +77,29 @@ export class MetricsTabCtrl {
     }
     }
 
 
     this.datasourceInstance = option.datasource;
     this.datasourceInstance = option.datasource;
-    this.panelCtrl.setDatasource(option.datasource);
+    this.setDatasource(option.datasource);
     this.updateDatasourceOptions();
     this.updateDatasourceOptions();
   }
   }
 
 
+  setDatasource(datasource) {
+    // switching to mixed
+    if (datasource.meta.mixed) {
+      _.each(this.panel.targets, target => {
+        target.datasource = this.panel.datasource;
+        if (!target.datasource) {
+          target.datasource = config.defaultDatasource;
+        }
+      });
+    } else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) {
+      _.each(this.panel.targets, target => {
+        delete target.datasource;
+      });
+    }
+
+    this.panel.datasource = datasource.value;
+    this.panel.refresh();
+  }
+
   addMixedQuery(option) {
   addMixedQuery(option) {
     if (!option) {
     if (!option) {
       return;
       return;
@@ -120,3 +150,5 @@ export function metricsTabDirective() {
     controller: MetricsTabCtrl,
     controller: MetricsTabCtrl,
   };
   };
 }
 }
+
+coreModule.directive('metricsTab', metricsTabDirective);

+ 17 - 20
public/app/features/panel/panel_ctrl.ts

@@ -24,10 +24,8 @@ export class PanelCtrl {
   $injector: any;
   $injector: any;
   $location: any;
   $location: any;
   $timeout: any;
   $timeout: any;
-  fullscreen: boolean;
   inspector: any;
   inspector: any;
   editModeInitiated: boolean;
   editModeInitiated: boolean;
-  editMode: any;
   height: any;
   height: any;
   containerHeight: any;
   containerHeight: any;
   events: Emitter;
   events: Emitter;
@@ -49,7 +47,6 @@ export class PanelCtrl {
       this.pluginName = plugin.name;
       this.pluginName = plugin.name;
     }
     }
 
 
-    $scope.$on('refresh', () => this.refresh());
     $scope.$on('component-did-mount', () => this.panelDidMount());
     $scope.$on('component-did-mount', () => this.panelDidMount());
 
 
     $scope.$on('$destroy', () => {
     $scope.$on('$destroy', () => {
@@ -58,13 +55,9 @@ export class PanelCtrl {
     });
     });
   }
   }
 
 
-  init() {
-    this.events.emit('panel-initialized');
-    this.publishAppEvent('panel-initialized', { scope: this.$scope });
-  }
-
   panelDidMount() {
   panelDidMount() {
     this.events.emit('component-did-mount');
     this.events.emit('component-did-mount');
+    this.dashboard.panelInitialized(this.panel);
   }
   }
 
 
   renderingCompleted() {
   renderingCompleted() {
@@ -72,7 +65,7 @@ export class PanelCtrl {
   }
   }
 
 
   refresh() {
   refresh() {
-    this.events.emit('refresh', null);
+    this.panel.refresh();
   }
   }
 
 
   publishAppEvent(evtName, evt) {
   publishAppEvent(evtName, evt) {
@@ -102,6 +95,7 @@ export class PanelCtrl {
   initEditMode() {
   initEditMode() {
     this.editorTabs = [];
     this.editorTabs = [];
     this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
     this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
+
     this.editModeInitiated = true;
     this.editModeInitiated = true;
     this.events.emit('init-edit-mode', null);
     this.events.emit('init-edit-mode', null);
 
 
@@ -122,14 +116,15 @@ export class PanelCtrl {
     route.updateParams();
     route.updateParams();
   }
   }
 
 
-  addEditorTab(title, directiveFn, index?) {
-    const editorTab = { title, directiveFn };
+  addEditorTab(title, directiveFn, index?, icon?) {
+    const editorTab = { title, directiveFn, icon };
 
 
     if (_.isString(directiveFn)) {
     if (_.isString(directiveFn)) {
       editorTab.directiveFn = () => {
       editorTab.directiveFn = () => {
         return { templateUrl: directiveFn };
         return { templateUrl: directiveFn };
       };
       };
     }
     }
+
     if (index) {
     if (index) {
       this.editorTabs.splice(index, 0, editorTab);
       this.editorTabs.splice(index, 0, editorTab);
     } else {
     } else {
@@ -190,7 +185,7 @@ export class PanelCtrl {
 
 
   getExtendedMenu() {
   getExtendedMenu() {
     const menu = [];
     const menu = [];
-    if (!this.fullscreen && this.dashboard.meta.canEdit) {
+    if (!this.panel.fullscreen && this.dashboard.meta.canEdit) {
       menu.push({
       menu.push({
         text: 'Duplicate',
         text: 'Duplicate',
         click: 'ctrl.duplicate()',
         click: 'ctrl.duplicate()',
@@ -220,15 +215,15 @@ export class PanelCtrl {
   }
   }
 
 
   otherPanelInFullscreenMode() {
   otherPanelInFullscreenMode() {
-    return this.dashboard.meta.fullscreen && !this.fullscreen;
+    return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
   }
   }
 
 
   calculatePanelHeight() {
   calculatePanelHeight() {
-    if (this.fullscreen) {
-      const docHeight = $(window).height();
-      const editHeight = Math.floor(docHeight * 0.4);
+    if (this.panel.fullscreen) {
+      const docHeight = $('.react-grid-layout').height();
+      const editHeight = Math.floor(docHeight * 0.35);
       const fullscreenHeight = Math.floor(docHeight * 0.8);
       const fullscreenHeight = Math.floor(docHeight * 0.8);
-      this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
+      this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight;
     } else {
     } else {
       this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
       this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
     }
     }
@@ -237,6 +232,11 @@ export class PanelCtrl {
       this.containerHeight = $(window).height();
       this.containerHeight = $(window).height();
     }
     }
 
 
+    // hacky solution
+    if (this.panel.isEditing && !this.editModeInitiated) {
+      this.initEditMode();
+    }
+
     this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT);
     this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT);
   }
   }
 
 
@@ -247,9 +247,6 @@ export class PanelCtrl {
 
 
   duplicate() {
   duplicate() {
     this.dashboard.duplicatePanel(this.panel);
     this.dashboard.duplicatePanel(this.panel);
-    this.$timeout(() => {
-      this.$scope.$root.$broadcast('render');
-    });
   }
   }
 
 
   removePanel() {
   removePanel() {

+ 43 - 44
public/app/features/panel/panel_directive.ts

@@ -6,48 +6,53 @@ import baron from 'baron';
 const module = angular.module('grafana.directives');
 const module = angular.module('grafana.directives');
 
 
 const panelTemplate = `
 const panelTemplate = `
-  <div class="panel-container">
-    <div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.fullscreen}">
-      <span class="panel-info-corner">
-        <i class="fa"></i>
-        <span class="panel-info-corner-inner"></span>
-      </span>
-
-      <span class="panel-loading" ng-show="ctrl.loading">
-        <i class="fa fa-spinner fa-spin"></i>
-      </span>
-
-      <panel-header class="panel-title-container" panel-ctrl="ctrl"></panel-header>
-    </div>
+  <div ng-class="{'panel-editor-container': ctrl.panel.isEditing, 'panel-height-helper': !ctrl.panel.isEditing}">
+    <div ng-class="{'panel-editor-container__panel': ctrl.panel.isEditing, 'panel-height-helper': !ctrl.panel.isEditing}">
+      <div class="panel-container">
+        <div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.panel.fullscreen}">
+          <span class="panel-info-corner">
+            <i class="fa"></i>
+            <span class="panel-info-corner-inner"></span>
+          </span>
+
+          <span class="panel-loading" ng-show="ctrl.loading">
+            <i class="fa fa-spinner fa-spin"></i>
+          </span>
+
+          <panel-header class="panel-title-container" panel-ctrl="ctrl"></panel-header>
+        </div>
 
 
-    <div class="panel-content">
-      <ng-transclude class="panel-height-helper"></ng-transclude>
+        <div class="panel-content">
+          <ng-transclude class="panel-height-helper"></ng-transclude>
+        </div>
+      </div>
     </div>
     </div>
-  </div>
 
 
-  <div class="panel-full-edit" ng-if="ctrl.editMode">
-    <div class="tabbed-view tabbed-view--panel-edit">
-      <div class="tabbed-view-header">
-        <h3 class="tabbed-view-panel-title">
-          {{ctrl.pluginName}}
-        </h3>
-
-        <ul class="gf-tabs">
-          <li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
-            <a class="gf-tabs-link" ng-click="ctrl.changeTab($index)" ng-class="{active: ctrl.editorTabIndex === $index}">
-              {{::tab.title}}
-            </a>
-          </li>
-        </ul>
-
-        <button class="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
-          <i class="fa fa-remove"></i>
-        </button>
-      </div>
+    <div ng-if="ctrl.panel.isEditing" ng-class="{'panel-editor-container__editor': ctrl.panel.isEditing,
+                                                 'panel-height-helper': !ctrl.panel.isEditing}">
+      <div class="tabbed-view tabbed-view--new">
+        <div class="tabbed-view-header">
+          <h3 class="tabbed-view-panel-title">
+            {{ctrl.pluginName}}
+          </h3>
+
+          <ul class="gf-tabs">
+            <li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
+              <a class="gf-tabs-link" ng-click="ctrl.changeTab($index)" ng-class="{active: ctrl.editorTabIndex === $index}">
+                {{::tab.title}}
+              </a>
+            </li>
+          </ul>
+
+          <button class="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
+            <i class="fa fa-remove"></i>
+          </button>
+        </div>
 
 
-      <div class="tabbed-view-body">
-        <div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index">
-          <panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
+        <div class="tabbed-view-body">
+          <div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index" class="panel-height-helper">
+            <panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
+          </div>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -85,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
         ctrl.dashboard.setPanelFocus(0);
         ctrl.dashboard.setPanelFocus(0);
       }
       }
 
 
-      function panelHeightUpdated() {
-        panelContent.css({ height: ctrl.height + 'px' });
-      }
-
       function resizeScrollableContent() {
       function resizeScrollableContent() {
         if (panelScrollbar) {
         if (panelScrollbar) {
           panelScrollbar.update();
           panelScrollbar.update();
@@ -133,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
 
 
       ctrl.events.on('panel-size-changed', () => {
       ctrl.events.on('panel-size-changed', () => {
         ctrl.calculatePanelHeight();
         ctrl.calculatePanelHeight();
-        panelHeightUpdated();
         $timeout(() => {
         $timeout(() => {
           resizeScrollableContent();
           resizeScrollableContent();
           ctrl.render();
           ctrl.render();
@@ -142,7 +142,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
 
 
       // set initial height
       // set initial height
       ctrl.calculatePanelHeight();
       ctrl.calculatePanelHeight();
-      panelHeightUpdated();
 
 
       ctrl.events.on('render', () => {
       ctrl.events.on('render', () => {
         if (transparentLastState !== ctrl.panel.transparent) {
         if (transparentLastState !== ctrl.panel.transparent) {

+ 18 - 10
public/app/features/panel/panel_editor_tab.ts

@@ -1,6 +1,7 @@
 import angular from 'angular';
 import angular from 'angular';
 
 
 const directiveModule = angular.module('grafana.directives');
 const directiveModule = angular.module('grafana.directives');
+const directiveCache = {};
 
 
 /** @ngInject */
 /** @ngInject */
 function panelEditorTab(dynamicDirectiveSrv) {
 function panelEditorTab(dynamicDirectiveSrv) {
@@ -12,17 +13,24 @@ function panelEditorTab(dynamicDirectiveSrv) {
     },
     },
     directive: scope => {
     directive: scope => {
       const pluginId = scope.ctrl.pluginId;
       const pluginId = scope.ctrl.pluginId;
-      const tabIndex = scope.index;
-      // create a wrapper for directiveFn
-      // required for metrics tab directive
-      // that is the same for many panels but
-      // given different names in this function
-      const fn = () => scope.editorTab.directiveFn();
+      const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-');
 
 
-      return Promise.resolve({
-        name: `panel-editor-tab-${pluginId}${tabIndex}`,
-        fn: fn,
-      });
+      if (directiveCache[pluginId]) {
+        if (directiveCache[pluginId][tabName]) {
+          return directiveCache[pluginId][tabName];
+        }
+      } else {
+        directiveCache[pluginId] = [];
+      }
+
+      const result = {
+        fn: () => scope.editorTab.directiveFn(),
+        name: `panel-editor-tab-${pluginId}${tabName}`,
+      };
+
+      directiveCache[pluginId][tabName] = result;
+
+      return result;
     },
     },
   });
   });
 }
 }

+ 0 - 15
public/app/features/panel/panel_header.ts

@@ -8,21 +8,6 @@ const template = `
   <span class="panel-menu-container dropdown">
   <span class="panel-menu-container dropdown">
     <span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
     <span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
     <ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
     <ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
-      <li>
-        <a ng-click="ctrl.addDataQuery(datasource);">
-          <i class="fa fa-cog"></i> Edit <span class="dropdown-menu-item-shortcut">e</span>
-        </a>
-      </li>
-      <li class="dropdown-submenu">
-        <a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-cube"></i> Actions</a>
-        <ul class="dropdown-menu panel-menu">
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-flash"></i> Add Annotation</a></li>
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-bullseye"></i> Toggle Legend</a></li>
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-download"></i> Export to CSV</a></li>
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-eye"></i> View JSON</a></li>
-        </ul>
-      </li>
-      <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li>
     </ul>
     </ul>
   </span>
   </span>
   <span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
   <span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>

+ 0 - 4
public/app/features/panel/partials/metrics_tab.html

@@ -1,11 +1,7 @@
 <div class="gf-form-group">
 <div class="gf-form-group">
   <div class="gf-form-inline">
   <div class="gf-form-inline">
     <div class="gf-form">
     <div class="gf-form">
-			<label class="gf-form-label gf-query-ds-label">
-				<i class="icon-gf icon-gf-datasources"></i>
-			</label>
       <label class="gf-form-label">Data Source</label>
       <label class="gf-form-label">Data Source</label>
-
       <gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
       <gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
                         lookup-text="true"
                         lookup-text="true"
                         get-options="ctrl.getOptions(true)"
                         get-options="ctrl.getOptions(true)"

+ 73 - 0
public/app/features/panel/viz_tab.ts

@@ -0,0 +1,73 @@
+import coreModule from 'app/core/core_module';
+import { DashboardModel } from '../dashboard/dashboard_model';
+import { VizTypePicker } from '../dashboard/dashgrid/VizTypePicker';
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+import { PanelPlugin } from 'app/types/plugins';
+
+export class VizTabCtrl {
+  panelCtrl: any;
+  dashboard: DashboardModel;
+
+  /** @ngInject */
+  constructor($scope) {
+    this.panelCtrl = $scope.ctrl;
+    this.dashboard = this.panelCtrl.dashboard;
+
+    $scope.ctrl = this;
+  }
+
+  onTypeChanged = (plugin: PanelPlugin) => {
+    this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id);
+  };
+}
+
+const template = `
+<div class="gf-form-group ">
+  <div class="gf-form-query">
+    <div class="gf-form">
+      <label class="gf-form-label">
+        <img src="public/app/plugins/panel/graph/img/icn-graph-panel.svg" style="width: 16px; height: 16px" />
+        Graph
+        <i class="fa fa-caret-down" />
+      </label>
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<label class="gf-form-label gf-form-label--grow"></label>
+		</div>
+	</div>
+
+	<br />
+	<br />
+
+  <div class="query-editor-rows gf-form-group">
+	  <div ng-repeat="tab in ctrl.panelCtrl.optionTabs">
+	    <div class="gf-form-query">
+		    <div class="gf-form gf-form-query-letter-cell">
+			    <label class="gf-form-label">
+				    <span class="gf-form-query-letter-cell-carret">
+					    <i class="fa fa-caret-down"></i>
+				    </span>
+				    <span class="gf-form-query-letter-cell-letter">{{tab.title}}</span>
+          </label>
+			  </div>
+        <div class="gf-form gf-form--grow">
+			    <label class="gf-form-label gf-form-label--grow"></label>
+		    </div>
+			</div>
+		</div>
+	</div>
+</div>`;
+
+/** @ngInject */
+export function vizTabDirective() {
+  'use strict';
+  return {
+    restrict: 'E',
+    template: template,
+    controller: VizTabCtrl,
+  };
+}
+
+react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]);
+coreModule.directive('vizTab', vizTabDirective);

+ 4 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -14,6 +14,8 @@ import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
 import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
 import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
 
 
 import * as textPanel from 'app/plugins/panel/text/module';
 import * as textPanel from 'app/plugins/panel/text/module';
+import * as text2Panel from 'app/plugins/panel/text2/module';
+import * as graph2Panel from 'app/plugins/panel/graph2/module';
 import * as graphPanel from 'app/plugins/panel/graph/module';
 import * as graphPanel from 'app/plugins/panel/graph/module';
 import * as dashListPanel from 'app/plugins/panel/dashlist/module';
 import * as dashListPanel from 'app/plugins/panel/dashlist/module';
 import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
 import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
@@ -40,6 +42,8 @@ const builtInPlugins = {
   'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
   'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
 
 
   'app/plugins/panel/text/module': textPanel,
   'app/plugins/panel/text/module': textPanel,
+  'app/plugins/panel/text2/module': text2Panel,
+  'app/plugins/panel/graph2/module': graph2Panel,
   'app/plugins/panel/graph/module': graphPanel,
   'app/plugins/panel/graph/module': graphPanel,
   'app/plugins/panel/dashlist/module': dashListPanel,
   'app/plugins/panel/dashlist/module': dashListPanel,
   'app/plugins/panel/pluginlist/module': pluginsListPanel,
   'app/plugins/panel/pluginlist/module': pluginsListPanel,

+ 17 - 1
public/app/features/plugins/datasource_srv.ts

@@ -1,8 +1,14 @@
+// Libraries
 import _ from 'lodash';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
+
+// Utils
 import config from 'app/core/config';
 import config from 'app/core/config';
 import { importPluginModule } from './plugin_loader';
 import { importPluginModule } from './plugin_loader';
 
 
+// Types
+import { DataSourceApi } from 'app/types/series';
+
 export class DatasourceSrv {
 export class DatasourceSrv {
   datasources: any;
   datasources: any;
 
 
@@ -15,7 +21,7 @@ export class DatasourceSrv {
     this.datasources = {};
     this.datasources = {};
   }
   }
 
 
-  get(name?) {
+  get(name?): Promise<DataSourceApi> {
     if (!name) {
     if (!name) {
       return this.get(config.defaultDatasource);
       return this.get(config.defaultDatasource);
     }
     }
@@ -162,5 +168,15 @@ export class DatasourceSrv {
   }
   }
 }
 }
 
 
+let singleton: DatasourceSrv;
+
+export function setDatasourceSrv(srv: DatasourceSrv) {
+  singleton = srv;
+}
+
+export function getDatasourceSrv(): DatasourceSrv {
+  return singleton;
+}
+
 coreModule.service('datasourceSrv', DatasourceSrv);
 coreModule.service('datasourceSrv', DatasourceSrv);
 export default DatasourceSrv;
 export default DatasourceSrv;

+ 8 - 6
public/app/features/plugins/plugin_component.ts

@@ -8,7 +8,7 @@ import { importPluginModule } from './plugin_loader';
 import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
 import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
 
 
 /** @ngInject */
 /** @ngInject */
-function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
+function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
   function getTemplate(component) {
   function getTemplate(component) {
     if (component.template) {
     if (component.template) {
       return $q.when(component.template);
       return $q.when(component.template);
@@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
 
 
       PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
       PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
         PanelCtrl.templateUrl = null;
         PanelCtrl.templateUrl = null;
-        PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-height-helper">${template}</grafana-panel>`;
+        PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-editor-container">${template}</grafana-panel>`;
         return componentInfo;
         return componentInfo;
       });
       });
 
 
@@ -207,10 +207,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
 
 
     // let a binding digest cycle complete before adding to dom
     // let a binding digest cycle complete before adding to dom
     setTimeout(() => {
     setTimeout(() => {
-      elem.append(child);
       scope.$applyAsync(() => {
       scope.$applyAsync(() => {
-        scope.$broadcast('component-did-mount');
-        scope.$broadcast('refresh');
+        elem.append(child);
+        setTimeout(() => {
+          scope.$applyAsync(() => {
+            scope.$broadcast('component-did-mount');
+          });
+        });
       });
       });
     });
     });
   }
   }
@@ -245,7 +248,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
           registerPluginComponent(scope, elem, attrs, componentInfo);
           registerPluginComponent(scope, elem, attrs, componentInfo);
         })
         })
         .catch(err => {
         .catch(err => {
-          $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
           console.log('Plugin component error', err);
           console.log('Plugin component error', err);
         });
         });
     },
     },

+ 3 - 1
public/app/features/plugins/plugin_loader.ts

@@ -18,6 +18,7 @@ import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
 import TableModel from 'app/core/table_model';
 import TableModel from 'app/core/table_model';
 import { coreModule, appEvents, contextSrv } from 'app/core/core';
 import { coreModule, appEvents, contextSrv } from 'app/core/core';
+import { PluginExports } from 'app/types/plugins';
 import * as datemath from 'app/core/utils/datemath';
 import * as datemath from 'app/core/utils/datemath';
 import * as fileExport from 'app/core/utils/file_export';
 import * as fileExport from 'app/core/utils/file_export';
 import * as flatten from 'app/core/utils/flatten';
 import * as flatten from 'app/core/utils/flatten';
@@ -140,11 +141,12 @@ const flotDeps = [
   'jquery.flot.events',
   'jquery.flot.events',
   'jquery.flot.gauge',
   'jquery.flot.gauge',
 ];
 ];
+
 for (const flotDep of flotDeps) {
 for (const flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });
   exposeToPlugin(flotDep, { fakeDep: 1 });
 }
 }
 
 
-export function importPluginModule(path: string): Promise<any> {
+export function importPluginModule(path: string): Promise<PluginExports> {
   const builtIn = builtInPlugins[path];
   const builtIn = builtInPlugins[path];
   if (builtIn) {
   if (builtIn) {
     return Promise.resolve(builtIn);
     return Promise.resolve(builtIn);

+ 7 - 4
public/app/features/templating/specs/variable_srv.test.ts

@@ -1,5 +1,6 @@
 import '../all';
 import '../all';
 import { VariableSrv } from '../variable_srv';
 import { VariableSrv } from '../variable_srv';
+import { DashboardModel } from '../../dashboard/dashboard_model';
 import moment from 'moment';
 import moment from 'moment';
 import $q from 'q';
 import $q from 'q';
 
 
@@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) {
           return getVarMockConstructor(ctr, model, ctx);
           return getVarMockConstructor(ctr, model, ctx);
         };
         };
 
 
-        ctx.variableSrv.init({
-          templating: { list: [] },
-          updateSubmenuVisibility: () => {},
-        });
+        ctx.variableSrv.init(
+          new DashboardModel({
+            templating: { list: [] },
+            updateSubmenuVisibility: () => {},
+          })
+        );
 
 
         scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
         scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
         ctx.variableSrv.addVariable(scenario.variable);
         ctx.variableSrv.addVariable(scenario.variable);

+ 3 - 2
public/app/features/templating/specs/variable_srv_init.test.ts

@@ -2,6 +2,7 @@ import '../all';
 
 
 import _ from 'lodash';
 import _ from 'lodash';
 import { VariableSrv } from '../variable_srv';
 import { VariableSrv } from '../variable_srv';
+import { DashboardModel } from '../../dashboard/dashboard_model';
 import $q from 'q';
 import $q from 'q';
 
 
 describe('VariableSrv init', function(this: any) {
 describe('VariableSrv init', function(this: any) {
@@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) {
         ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
         ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
 
 
         ctx.variableSrv.$location.search = () => scenario.urlParams;
         ctx.variableSrv.$location.search = () => scenario.urlParams;
-        ctx.variableSrv.dashboard = {
+        ctx.variableSrv.dashboard = new DashboardModel({
           templating: { list: scenario.variables },
           templating: { list: scenario.variables },
-        };
+        });
 
 
         await ctx.variableSrv.init(ctx.variableSrv.dashboard);
         await ctx.variableSrv.init(ctx.variableSrv.dashboard);
 
 

+ 9 - 9
public/app/features/templating/variable_srv.ts

@@ -1,5 +1,8 @@
+// Libaries
 import angular from 'angular';
 import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
+
+// Utils & Services
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { variableTypes } from './variable';
 import { variableTypes } from './variable';
 import { Graph } from 'app/core/utils/dag';
 import { Graph } from 'app/core/utils/dag';
@@ -10,13 +13,12 @@ export class VariableSrv {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
   constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
-    // update time variant variables
-    $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope);
     $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
     $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
   }
   }
 
 
   init(dashboard) {
   init(dashboard) {
     this.dashboard = dashboard;
     this.dashboard = dashboard;
+    this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this));
 
 
     // create working class models representing variables
     // create working class models representing variables
     this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
     this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
@@ -39,11 +41,7 @@ export class VariableSrv {
       });
       });
   }
   }
 
 
-  onDashboardRefresh(evt, payload) {
-    if (payload && payload.fromVariableValueUpdated) {
-      return Promise.resolve({});
-    }
-
+  onTimeRangeUpdated() {
     const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
     const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
       const previousOptions = variable.options.slice();
       const previousOptions = variable.options.slice();
 
 
@@ -54,7 +52,9 @@ export class VariableSrv {
       });
       });
     });
     });
 
 
-    return this.$q.all(promises);
+    return this.$q.all(promises).then(() => {
+      this.dashboard.startRefresh();
+    });
   }
   }
 
 
   processVariable(variable, queryParams) {
   processVariable(variable, queryParams) {
@@ -133,7 +133,7 @@ export class VariableSrv {
     return this.$q.all(promises).then(() => {
     return this.$q.all(promises).then(() => {
       if (emitChangeEvents) {
       if (emitChangeEvents) {
         this.$rootScope.$emit('template-variable-value-updated');
         this.$rootScope.$emit('template-variable-value-updated');
-        this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true });
+        this.dashboard.startRefresh();
       }
       }
     });
     });
   }
   }

+ 2 - 3
public/app/partials/dashboard.html

@@ -7,12 +7,11 @@
                         class="dashboard-settings">
                         class="dashboard-settings">
     </dashboard-settings>
     </dashboard-settings>
 
 
-    <div class="dashboard-container">
+		<div class="dashboard-container" ng-class="{'dashboard-container--has-submenu': ctrl.dashboard.meta.submenuEnabled}">
       <dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
       <dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
       </dashboard-submenu>
       </dashboard-submenu>
 
 
-      <dashboard-grid get-panel-container="ctrl.getPanelContainer">
-      </dashboard-grid>
+      <dashboard-grid dashboard="ctrl.dashboard"></dashboard-grid>
     </div>
     </div>
   </div>
   </div>
 </div>
 </div>

+ 5 - 1
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -137,7 +137,11 @@ export default class CloudWatchDatasource {
       if (res.results) {
       if (res.results) {
         _.forEach(res.results, queryRes => {
         _.forEach(res.results, queryRes => {
           _.forEach(queryRes.series, series => {
           _.forEach(queryRes.series, series => {
-            data.push({ target: series.name, datapoints: series.points, unit: queryRes.meta.unit || 'none' });
+            const s = { target: series.name, datapoints: series.points } as any;
+            if (queryRes.meta.unit) {
+              s.unit = queryRes.meta.unit;
+            }
+            data.push(s);
           });
           });
         });
         });
       }
       }

+ 3 - 2
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts

@@ -1,4 +1,5 @@
 import angular from 'angular';
 import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 export class CloudWatchQueryParameter {
 export class CloudWatchQueryParameter {
@@ -239,5 +240,5 @@ export class CloudWatchQueryParameterCtrl {
   }
   }
 }
 }
 
 
-angular.module('grafana.controllers').directive('cloudwatchQueryParameter', CloudWatchQueryParameter);
-angular.module('grafana.controllers').controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl);
+coreModule.directive('cloudwatchQueryParameter', CloudWatchQueryParameter);
+coreModule.controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl);

+ 3 - 4
public/app/plugins/datasource/elasticsearch/bucket_agg.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import _ from 'lodash';
 import * as queryDef from './query_def';
 import * as queryDef from './query_def';
 
 
@@ -226,6 +226,5 @@ export class ElasticBucketAggCtrl {
   }
   }
 }
 }
 
 
-const module = angular.module('grafana.directives');
-module.directive('elasticBucketAgg', elasticBucketAgg);
-module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);
+coreModule.directive('elasticBucketAgg', elasticBucketAgg);
+coreModule.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);

+ 3 - 4
public/app/plugins/datasource/elasticsearch/metric_agg.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import _ from 'lodash';
 import * as queryDef from './query_def';
 import * as queryDef from './query_def';
 
 
@@ -203,6 +203,5 @@ export class ElasticMetricAggCtrl {
   }
   }
 }
 }
 
 
-const module = angular.module('grafana.directives');
-module.directive('elasticMetricAgg', elasticMetricAgg);
-module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);
+coreModule.directive('elasticMetricAgg', elasticMetricAgg);
+coreModule.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);

+ 2 - 2
public/app/plugins/datasource/graphite/add_graphite_func.ts

@@ -1,8 +1,8 @@
-import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
 import rst2html from 'rst2html';
 import rst2html from 'rst2html';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
+import coreModule from 'app/core/core_module';
 
 
 /** @ngInject */
 /** @ngInject */
 export function graphiteAddFunc($compile) {
 export function graphiteAddFunc($compile) {
@@ -130,7 +130,7 @@ export function graphiteAddFunc($compile) {
   };
   };
 }
 }
 
 
-angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc);
+coreModule.directive('graphiteAddFunc', graphiteAddFunc);
 
 
 function createFunctionDropDownMenu(funcDefs) {
 function createFunctionDropDownMenu(funcDefs) {
   const categories = {};
   const categories = {};

+ 2 - 2
public/app/plugins/datasource/graphite/func_editor.ts

@@ -1,7 +1,7 @@
-import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
 import rst2html from 'rst2html';
 import rst2html from 'rst2html';
+import coreModule from 'app/core/core_module';
 
 
 /** @ngInject */
 /** @ngInject */
 export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
 export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
@@ -315,4 +315,4 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
   };
   };
 }
 }
 
 
-angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor);
+coreModule.directive('graphiteFuncEditor', graphiteFuncEditor);

+ 15 - 1
public/app/plugins/datasource/stackdriver/config_ctrl.ts

@@ -5,13 +5,23 @@ export class StackdriverConfigCtrl {
   jsonText: string;
   jsonText: string;
   validationErrors: string[] = [];
   validationErrors: string[] = [];
   inputDataValid: boolean;
   inputDataValid: boolean;
+  authenticationTypes: any[];
+  defaultAuthenticationType: string;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(datasourceSrv) {
   constructor(datasourceSrv) {
+    this.defaultAuthenticationType = 'jwt';
     this.datasourceSrv = datasourceSrv;
     this.datasourceSrv = datasourceSrv;
     this.current.jsonData = this.current.jsonData || {};
     this.current.jsonData = this.current.jsonData || {};
+    this.current.jsonData.authenticationType = this.current.jsonData.authenticationType
+      ? this.current.jsonData.authenticationType
+      : this.defaultAuthenticationType;
     this.current.secureJsonData = this.current.secureJsonData || {};
     this.current.secureJsonData = this.current.secureJsonData || {};
     this.current.secureJsonFields = this.current.secureJsonFields || {};
     this.current.secureJsonFields = this.current.secureJsonFields || {};
+    this.authenticationTypes = [
+      { key: this.defaultAuthenticationType, value: 'Google JWT File' },
+      { key: 'gce', value: 'GCE Default Service Account' },
+    ];
   }
   }
 
 
   save(jwt) {
   save(jwt) {
@@ -35,6 +45,10 @@ export class StackdriverConfigCtrl {
       this.validationErrors.push('Client Email field missing in JWT file.');
       this.validationErrors.push('Client Email field missing in JWT file.');
     }
     }
 
 
+    if (!jwt.project_id || jwt.project_id.length === 0) {
+      this.validationErrors.push('Project Id field missing in JWT file.');
+    }
+
     if (this.validationErrors.length === 0) {
     if (this.validationErrors.length === 0) {
       this.inputDataValid = true;
       this.inputDataValid = true;
       return true;
       return true;
@@ -67,7 +81,7 @@ export class StackdriverConfigCtrl {
     this.inputDataValid = false;
     this.inputDataValid = false;
     this.jsonText = '';
     this.jsonText = '';
 
 
-    this.current.jsonData = {};
+    this.current.jsonData = Object.assign({}, { authenticationType: this.current.jsonData.authenticationType });
     this.current.secureJsonData = {};
     this.current.secureJsonData = {};
     this.current.secureJsonFields = {};
     this.current.secureJsonFields = {};
   }
   }

+ 109 - 86
public/app/plugins/datasource/stackdriver/datasource.ts

@@ -1,11 +1,14 @@
 import { stackdriverUnitMappings } from './constants';
 import { stackdriverUnitMappings } from './constants';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
+import _ from 'lodash';
 
 
 export default class StackdriverDatasource {
 export default class StackdriverDatasource {
   id: number;
   id: number;
   url: string;
   url: string;
   baseUrl: string;
   baseUrl: string;
   projectName: string;
   projectName: string;
+  authenticationType: string;
+  queryPromise: Promise<any>;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
   constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
@@ -14,6 +17,7 @@ export default class StackdriverDatasource {
     this.doRequest = this.doRequest;
     this.doRequest = this.doRequest;
     this.id = instanceSettings.id;
     this.id = instanceSettings.id;
     this.projectName = instanceSettings.jsonData.defaultProject || '';
     this.projectName = instanceSettings.jsonData.defaultProject || '';
+    this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
   }
   }
 
 
   async getTimeSeries(options) {
   async getTimeSeries(options) {
@@ -46,16 +50,20 @@ export default class StackdriverDatasource {
         };
         };
       });
       });
 
 
-    const { data } = await this.backendSrv.datasourceRequest({
-      url: '/api/tsdb/query',
-      method: 'POST',
-      data: {
-        from: options.range.from.valueOf().toString(),
-        to: options.range.to.valueOf().toString(),
-        queries,
-      },
-    });
-    return data;
+    if (queries.length > 0) {
+      const { data } = await this.backendSrv.datasourceRequest({
+        url: '/api/tsdb/query',
+        method: 'POST',
+        data: {
+          from: options.range.from.valueOf().toString(),
+          to: options.range.to.valueOf().toString(),
+          queries,
+        },
+      });
+      return data;
+    } else {
+      return { results: [] };
+    }
   }
   }
 
 
   async getLabels(metricType, refId) {
   async getLabels(metricType, refId) {
@@ -89,7 +97,7 @@ export default class StackdriverDatasource {
   }
   }
 
 
   resolvePanelUnitFromTargets(targets: any[]) {
   resolvePanelUnitFromTargets(targets: any[]) {
-    let unit = 'none';
+    let unit;
     if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
     if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
       if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) {
       if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) {
         unit = stackdriverUnitMappings[targets[0].unit];
         unit = stackdriverUnitMappings[targets[0].unit];
@@ -99,28 +107,34 @@ export default class StackdriverDatasource {
   }
   }
 
 
   async query(options) {
   async query(options) {
-    const result = [];
-    const data = await this.getTimeSeries(options);
-    if (data.results) {
-      Object['values'](data.results).forEach(queryRes => {
-        if (!queryRes.series) {
-          return;
-        }
-
-        const unit = this.resolvePanelUnitFromTargets(options.targets);
-        queryRes.series.forEach(series => {
-          result.push({
-            target: series.name,
-            datapoints: series.points,
-            refId: queryRes.refId,
-            meta: queryRes.meta,
-            unit,
+    this.queryPromise = new Promise(async resolve => {
+      const result = [];
+      const data = await this.getTimeSeries(options);
+      if (data.results) {
+        Object['values'](data.results).forEach(queryRes => {
+          if (!queryRes.series) {
+            return;
+          }
+          this.projectName = queryRes.meta.defaultProject;
+          const unit = this.resolvePanelUnitFromTargets(options.targets);
+          queryRes.series.forEach(series => {
+            let timeSerie: any = {
+              target: series.name,
+              datapoints: series.points,
+              refId: queryRes.refId,
+              meta: queryRes.meta,
+            };
+            if (unit) {
+              timeSerie = { ...timeSerie, unit };
+            }
+            result.push(timeSerie);
           });
           });
         });
         });
-      });
-    }
+      }
 
 
-    return { data: result };
+      resolve({ data: result });
+    });
+    return this.queryPromise;
   }
   }
 
 
   async annotationQuery(options) {
   async annotationQuery(options) {
@@ -170,76 +184,84 @@ export default class StackdriverDatasource {
     throw new Error('Template variables support is not yet imlemented');
     throw new Error('Template variables support is not yet imlemented');
   }
   }
 
 
-  testDatasource() {
-    const path = `v3/projects/${this.projectName}/metricDescriptors`;
-    return this.doRequest(`${this.baseUrl}${path}`)
-      .then(response => {
-        if (response.status === 200) {
-          return {
-            status: 'success',
-            message: 'Successfully queried the Stackdriver API.',
-            title: 'Success',
-          };
-        }
-
-        return {
-          status: 'error',
-          message: 'Returned http status code ' + response.status,
-        };
-      })
-      .catch(error => {
-        let message = 'Stackdriver: ';
-        message += error.statusText ? error.statusText + ': ' : '';
-
+  async testDatasource() {
+    let status, message;
+    const defaultErrorMessage = 'Cannot connect to Stackdriver API';
+    try {
+      const projectName = await this.getDefaultProject();
+      const path = `v3/projects/${projectName}/metricDescriptors`;
+      const response = await this.doRequest(`${this.baseUrl}${path}`);
+      if (response.status === 200) {
+        status = 'success';
+        message = 'Successfully queried the Stackdriver API.';
+      } else {
+        status = 'error';
+        message = response.statusText ? response.statusText : defaultErrorMessage;
+      }
+    } catch (error) {
+      status = 'error';
+      if (_.isString(error)) {
+        message = error;
+      } else {
+        message = 'Stackdriver: ';
+        message += error.statusText ? error.statusText : defaultErrorMessage;
         if (error.data && error.data.error && error.data.error.code) {
         if (error.data && error.data.error && error.data.error.code) {
-          // 400, 401
-          message += error.data.error.code + '. ' + error.data.error.message;
-        } else {
-          message += 'Cannot connect to Stackdriver API';
+          message += ': ' + error.data.error.code + '. ' + error.data.error.message;
         }
         }
-        return {
-          status: 'error',
-          message: message,
-        };
-      });
+      }
+    } finally {
+      return {
+        status,
+        message,
+      };
+    }
   }
   }
 
 
-  async getProjects() {
-    const response = await this.doRequest(`/cloudresourcemanager/v1/projects`);
-    return response.data.projects.map(p => ({ id: p.projectId, name: p.name }));
+  formatStackdriverError(error) {
+    let message = 'Stackdriver: ';
+    message += error.statusText ? error.statusText + ': ' : '';
+    if (error.data && error.data.error) {
+      try {
+        const res = JSON.parse(error.data.error);
+        message += res.error.code + '. ' + res.error.message;
+      } catch (err) {
+        message += error.data.error;
+      }
+    } else {
+      message += 'Cannot connect to Stackdriver API';
+    }
+    return message;
   }
   }
 
 
   async getDefaultProject() {
   async getDefaultProject() {
     try {
     try {
-      const projects = await this.getProjects();
-      if (projects && projects.length > 0) {
-        const test = projects.filter(p => p.id === this.projectName)[0];
-        return test;
+      if (this.authenticationType === 'gce' || !this.projectName) {
+        const { data } = await this.backendSrv.datasourceRequest({
+          url: '/api/tsdb/query',
+          method: 'POST',
+          data: {
+            queries: [
+              {
+                refId: 'ensureDefaultProjectQuery',
+                type: 'ensureDefaultProjectQuery',
+                datasourceId: this.id,
+              },
+            ],
+          },
+        });
+        this.projectName = data.results.ensureDefaultProjectQuery.meta.defaultProject;
+        return this.projectName;
       } else {
       } else {
-        throw new Error('No projects found');
+        return this.projectName;
       }
       }
     } catch (error) {
     } catch (error) {
-      let message = 'Projects cannot be fetched: ';
-      message += error.statusText ? error.statusText + ': ' : '';
-      if (error && error.data && error.data.error && error.data.error.message) {
-        if (error.data.error.code === 403) {
-          message += `
-            A list of projects could not be fetched from the Google Cloud Resource Manager API.
-            You might need to enable it first:
-            https://console.developers.google.com/apis/library/cloudresourcemanager.googleapis.com`;
-        } else {
-          message += error.data.error.code + '. ' + error.data.error.message;
-        }
-      } else {
-        message += 'Cannot connect to Stackdriver API';
-      }
-      appEvents.emit('ds-request-error', message);
+      throw this.formatStackdriverError(error);
     }
     }
   }
   }
 
 
-  async getMetricTypes(projectId: string) {
+  async getMetricTypes(projectName: string) {
     try {
     try {
-      const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`;
+      const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
       const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
       const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
 
 
       const metrics = data.metricDescriptors.map(m => {
       const metrics = data.metricDescriptors.map(m => {
@@ -253,7 +275,8 @@ export default class StackdriverDatasource {
 
 
       return metrics;
       return metrics;
     } catch (error) {
     } catch (error) {
-      console.log(error);
+      appEvents.emit('ds-request-error', this.formatStackdriverError(error));
+      return [];
     }
     }
   }
   }
 
 

+ 42 - 23
public/app/plugins/datasource/stackdriver/partials/config.html

@@ -1,37 +1,54 @@
 <div class="gf-form-group">
 <div class="gf-form-group">
   <div class="grafana-info-box">
   <div class="grafana-info-box">
-    <h5>GCP Service Account</h5>
+    <h4>Stackdriver Authentication</h4>
+    <p>There are two ways to authenticate the Stackdriver plugin - either by uploading a Service Account key file, or by
+      automatically retrieving credentials from the Google metadata server. The latter option is only available
+      when running Grafana on a GCE virtual machine.</p>
+
+    <h5>Uploading a Service Account Key File</h5>
     <p>
     <p>
-      To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for
+      First you need to create a Google Cloud Platform (GCP) Service Account for
       the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to
       the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to
       visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
       visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
     </p>
     </p>
     <p>
     <p>
-      The <strong>Monitoring Viewer</strong> role provides all the permissions that Grafana needs.
+      The <strong>Monitoring Viewer</strong> role provides all the permissions that Grafana needs. The following API
+      needs to be enabled on GCP for the datasource to work: <a class="external-link" target="_blank" href="https://console.cloud.google.com/apis/library/monitoring.googleapis.com">Monitoring
+        API</a>
     </p>
     </p>
+
+    <h5>GCE Default Service Account</h5>
     <p>
     <p>
-      The following APIs need to be enabled on GCP for the datasource to work:
-      <ul>
-        <li><a class="external-link" target="_blank" href="https://console.cloud.google.com/apis/library/monitoring.googleapis.com">Monitoring
-            API</a></li>
-        <li><a class="external-link" target="_blank" href="https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com">Resource
-            Manager API</a></li>
-      </ul>
+      If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to
+      automatically retrieve the default project id and authentication token from the metadata server. In order for this to
+      work, you need to make sure that you have a service account that is setup as the default account for the virtual
+      machine and that the service account has been given read access to the Stackdriver Monitoring API.
     </p>
     </p>
+
     <p>Detailed instructions on how to create a Service Account can be found <a class="external-link" target="_blank"
     <p>Detailed instructions on how to create a Service Account can be found <a class="external-link" target="_blank"
         href="http://docs.grafana.org/datasources/stackdriver/">in
         href="http://docs.grafana.org/datasources/stackdriver/">in
-        the documentation.</a></p>
+        the documentation.</a>
+    </p>
   </div>
   </div>
 </div>
 </div>
 
 
 <div class="gf-form-group">
 <div class="gf-form-group">
   <div class="gf-form">
   <div class="gf-form">
-    <h3>Service Account Authentication</h3>
+    <h3>Authentication</h3>
     <info-popover mode="header">Upload your Service Account key file or paste in the contents of the file. The file
     <info-popover mode="header">Upload your Service Account key file or paste in the contents of the file. The file
       contents will be encrypted and saved in the Grafana database.</info-popover>
       contents will be encrypted and saved in the Grafana database.</info-popover>
   </div>
   </div>
 
 
-  <div ng-if="!ctrl.current.jsonData.clientEmail && !ctrl.inputDataValid">
+  <div class="gf-form-inline">
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-10">Authentication Type</span>
+      <div class="gf-form-select-wrapper max-width-24">
+        <select class="gf-form-input" ng-model="ctrl.current.jsonData.authenticationType" ng-options="f.key as f.value for f in ctrl.authenticationTypes"></select>
+      </div>
+    </div>
+  </div>
+
+  <div ng-if="ctrl.current.jsonData.authenticationType === ctrl.defaultAuthenticationType && !ctrl.current.jsonData.clientEmail && !ctrl.inputDataValid">
     <div class="gf-form-group" ng-if="!ctrl.inputDataValid">
     <div class="gf-form-group" ng-if="!ctrl.inputDataValid">
       <div class="gf-form">
       <div class="gf-form">
         <form>
         <form>
@@ -52,23 +69,23 @@
   </div>
   </div>
 </div>
 </div>
 
 
-<div class="gf-form-group" ng-if="ctrl.inputDataValid || ctrl.current.jsonData.clientEmail">
+<div class="gf-form-group" ng-if="ctrl.current.jsonData.authenticationType === ctrl.defaultAuthenticationType && (ctrl.inputDataValid || ctrl.current.jsonData.clientEmail)">
   <h6>Uploaded Key Details</h6>
   <h6>Uploaded Key Details</h6>
 
 
   <div class="gf-form">
   <div class="gf-form">
-    <span class="gf-form-label width-9">Project</span>
+    <span class="gf-form-label width-10">Project</span>
     <input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.defaultProject" />
     <input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.defaultProject" />
   </div>
   </div>
   <div class="gf-form">
   <div class="gf-form">
-      <span class="gf-form-label width-9">Client Email</span>
-      <input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.clientEmail" />
-    </div>
+    <span class="gf-form-label width-10">Client Email</span>
+    <input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.clientEmail" />
+  </div>
   <div class="gf-form">
   <div class="gf-form">
-    <span class="gf-form-label width-9">Token URI</span>
+    <span class="gf-form-label width-10">Token URI</span>
     <input class="gf-form-input width-40" disabled type="text" ng-model='ctrl.current.jsonData.tokenUri' />
     <input class="gf-form-input width-40" disabled type="text" ng-model='ctrl.current.jsonData.tokenUri' />
   </div>
   </div>
   <div class="gf-form" ng-if="ctrl.current.secureJsonFields.privateKey">
   <div class="gf-form" ng-if="ctrl.current.secureJsonFields.privateKey">
-    <span class="gf-form-label width-9">Private Key</span>
+    <span class="gf-form-label width-10">Private Key</span>
     <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
     <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
   </div>
   </div>
 
 
@@ -81,6 +98,8 @@
   </div>
   </div>
 </div>
 </div>
 
 
-<div class="grafana-info-box" ng-hide="ctrl.current.secureJsonFields.privateKey">
-	Do not forget to save your changes after uploading a file.
-</div>
+<p class="gf-form-label" ng-hide="ctrl.current.secureJsonFields.privateKey || ctrl.current.jsonData.authenticationType !== ctrl.defaultAuthenticationType"><i
+    class="fa fa-save"></i> Do not forget to save your changes after uploading a file.</p>
+
+<p class="gf-form-label" ng-show="ctrl.current.jsonData.authenticationType !== ctrl.defaultAuthenticationType"><i class="fa fa-save"></i>
+  Verify GCE default service account by clicking Save & Test</p>

+ 3 - 4
public/app/plugins/datasource/stackdriver/partials/query.editor.html

@@ -15,8 +15,7 @@
   <div class="gf-form-inline">
   <div class="gf-form-inline">
     <div class="gf-form">
     <div class="gf-form">
       <span class="gf-form-label width-9">Project</span>
       <span class="gf-form-label width-9">Project</span>
-      <input class="gf-form-input" disabled type="text" ng-model='ctrl.target.project.name' get-options="ctrl.getProjects()"
-        css-class="min-width-12" />
+      <input class="gf-form-input" disabled type="text" ng-model='ctrl.target.defaultProject' css-class="min-width-12" />
     </div>
     </div>
     <div class="gf-form">
     <div class="gf-form">
       <label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
       <label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
@@ -40,8 +39,8 @@
   <div class="gf-form" ng-show="ctrl.showLastQuery">
   <div class="gf-form" ng-show="ctrl.showLastQuery">
     <pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
     <pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
   </div>
   </div>
-  <div class="grafana-info-box m-t-2 markdown-html" ng-show="ctrl.showHelp">
-    <h5>Alias Patterns</h5>
+  <div class="gf-form grafana-info-box" style="padding: 0" ng-show="ctrl.showHelp">
+    <pre class="gf-form-pre alert alert-info" style="margin-right: 0"><h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns.
 
 
     Format the legend keys any way you want by using alias patterns.<br /> <br />
     Format the legend keys any way you want by using alias patterns.<br /> <br />
 
 

+ 1 - 4
public/app/plugins/datasource/stackdriver/plugin.json

@@ -28,10 +28,7 @@
       "method": "GET",
       "method": "GET",
       "url": "https://content-monitoring.googleapis.com",
       "url": "https://content-monitoring.googleapis.com",
       "jwtTokenAuth": {
       "jwtTokenAuth": {
-        "scopes": [
-          "https://www.googleapis.com/auth/monitoring.read",
-          "https://www.googleapis.com/auth/cloudplatformprojects.readonly"
-        ],
+        "scopes": ["https://www.googleapis.com/auth/monitoring.read"],
         "params": {
         "params": {
           "token_uri": "{{.JsonData.tokenUri}}",
           "token_uri": "{{.JsonData.tokenUri}}",
           "client_email": "{{.JsonData.clientEmail}}",
           "client_email": "{{.JsonData.clientEmail}}",

+ 3 - 3
public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import _ from 'lodash';
 import * as options from './constants';
 import * as options from './constants';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
@@ -83,5 +83,5 @@ export class StackdriverAggregationCtrl {
   }
   }
 }
 }
 
 
-angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation);
-angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);
+coreModule.directive('stackdriverAggregation', StackdriverAggregation);
+coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);

+ 2 - 8
public/app/plugins/datasource/stackdriver/query_ctrl.ts

@@ -14,10 +14,7 @@ export interface QueryMeta {
 export class StackdriverQueryCtrl extends QueryCtrl {
 export class StackdriverQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
   static templateUrl = 'partials/query.editor.html';
   target: {
   target: {
-    project: {
-      id: string;
-      name: string;
-    };
+    defaultProject: string;
     unit: string;
     unit: string;
     metricType: string;
     metricType: string;
     service: string;
     service: string;
@@ -38,10 +35,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
   defaultServiceValue = 'All Services';
   defaultServiceValue = 'All Services';
 
 
   defaults = {
   defaults = {
-    project: {
-      id: 'default',
-      name: 'loading project...',
-    },
+    defaultProject: 'loading project...',
     metricType: this.defaultDropdownValue,
     metricType: this.defaultDropdownValue,
     service: this.defaultServiceValue,
     service: this.defaultServiceValue,
     metric: '',
     metric: '',

+ 16 - 6
public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import _ from 'lodash';
 import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
 import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
@@ -79,12 +79,22 @@ export class StackdriverFilterCtrl {
   }
   }
 
 
   async getCurrentProject() {
   async getCurrentProject() {
-    this.target.project = await this.datasource.getDefaultProject();
+    return new Promise(async (resolve, reject) => {
+      try {
+        if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') {
+          this.target.defaultProject = await this.datasource.getDefaultProject();
+        }
+        resolve(this.target.defaultProject);
+      } catch (error) {
+        appEvents.emit('ds-request-error', error);
+        reject();
+      }
+    });
   }
   }
 
 
   async loadMetricDescriptors() {
   async loadMetricDescriptors() {
-    if (this.target.project.id !== 'default') {
-      this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id);
+    if (this.target.defaultProject !== 'loading project...') {
+      this.metricDescriptors = await this.datasource.getMetricTypes(this.target.defaultProject);
       this.services = this.getServicesList();
       this.services = this.getServicesList();
       this.metrics = this.getMetricsList();
       this.metrics = this.getMetricsList();
       return this.metricDescriptors;
       return this.metricDescriptors;
@@ -281,5 +291,5 @@ export class StackdriverFilterCtrl {
   }
   }
 }
 }
 
 
-angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter);
-angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl);
+coreModule.directive('stackdriverFilter', StackdriverFilter);
+coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl);

+ 10 - 45
public/app/plugins/datasource/stackdriver/specs/datasource.test.ts

@@ -6,7 +6,7 @@ import { TemplateSrvStub } from 'test/specs/helpers';
 describe('StackdriverDataSource', () => {
 describe('StackdriverDataSource', () => {
   const instanceSettings = {
   const instanceSettings = {
     jsonData: {
     jsonData: {
-      projectName: 'testproject',
+      defaultProject: 'testproject',
     },
     },
   };
   };
   const templateSrv = new TemplateSrvStub();
   const templateSrv = new TemplateSrvStub();
@@ -53,7 +53,9 @@ describe('StackdriverDataSource', () => {
           datasourceRequest: async () =>
           datasourceRequest: async () =>
             Promise.reject({
             Promise.reject({
               statusText: 'Bad Request',
               statusText: 'Bad Request',
-              data: { error: { code: 400, message: 'Field interval.endTime had an invalid value' } },
+              data: {
+                error: { code: 400, message: 'Field interval.endTime had an invalid value' },
+              },
             }),
             }),
         };
         };
         ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv);
         ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv);
@@ -67,43 +69,6 @@ describe('StackdriverDataSource', () => {
     });
     });
   });
   });
 
 
-  describe('when performing getProjects', () => {
-    describe('and call to resource manager api succeeds', () => {
-      let ds;
-      let result;
-      beforeEach(async () => {
-        const response = {
-          projects: [
-            {
-              projectNumber: '853996325002',
-              projectId: 'test-project',
-              lifecycleState: 'ACTIVE',
-              name: 'Test Project',
-              createTime: '2015-06-02T14:16:08.520Z',
-              parent: {
-                type: 'organization',
-                id: '853996325002',
-              },
-            },
-          ],
-        };
-        const backendSrv = {
-          async datasourceRequest() {
-            return Promise.resolve({ status: 200, data: response });
-          },
-        };
-        ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv);
-        result = await ds.getProjects();
-      });
-
-      it('should return successfully', () => {
-        expect(result.length).toBe(1);
-        expect(result[0].id).toBe('test-project');
-        expect(result[0].name).toBe('Test Project');
-      });
-    });
-  });
-
   describe('When performing query', () => {
   describe('When performing query', () => {
     const options = {
     const options = {
       range: {
       range: {
@@ -235,8 +200,8 @@ describe('StackdriverDataSource', () => {
         beforeEach(() => {
         beforeEach(() => {
           res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
           res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
         });
         });
-        it('should return none', () => {
-          expect(res).toEqual('none');
+        it('should return undefined', () => {
+          expect(res).toBeUndefined();
         });
         });
       });
       });
       describe('and the stackdriver unit has a corresponding grafana unit', () => {
       describe('and the stackdriver unit has a corresponding grafana unit', () => {
@@ -262,16 +227,16 @@ describe('StackdriverDataSource', () => {
         beforeEach(() => {
         beforeEach(() => {
           res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
           res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
         });
         });
-        it('should return the default value - none', () => {
-          expect(res).toEqual('none');
+        it('should return the default value of undefined', () => {
+          expect(res).toBeUndefined();
         });
         });
       });
       });
       describe('and all target units are not the same', () => {
       describe('and all target units are not the same', () => {
         beforeEach(() => {
         beforeEach(() => {
           res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
           res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
         });
         });
-        it('should return the default value - none', () => {
-          expect(res).toEqual('none');
+        it('should return the default value of undefined', () => {
+          expect(res).toBeUndefined();
         });
         });
       });
       });
     });
     });

+ 0 - 1
public/app/plugins/datasource/testdata/datasource.ts

@@ -62,7 +62,6 @@ class TestDataDatasource {
           });
           });
         }
         }
 
 
-        console.log(res);
         return { data: data };
         return { data: data };
       });
       });
   }
   }

+ 6 - 5
public/app/plugins/panel/graph/legend.ts

@@ -1,11 +1,10 @@
-import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
 import baron from 'baron';
 import baron from 'baron';
+import coreModule from 'app/core/core_module';
 
 
-const module = angular.module('grafana.directives');
-
-module.directive('graphLegend', (popoverSrv, $timeout) => {
+/** @ngInject */
+function graphLegendDirective(popoverSrv, $timeout) {
   return {
   return {
     link: (scope, elem) => {
     link: (scope, elem) => {
       let firstRender = true;
       let firstRender = true;
@@ -302,4 +301,6 @@ module.directive('graphLegend', (popoverSrv, $timeout) => {
       }
       }
     },
     },
   };
   };
-});
+}
+
+coreModule.directive('graphLegend', graphLegendDirective);

+ 1 - 1
public/app/plugins/panel/graph/module.ts

@@ -134,9 +134,9 @@ class GraphCtrl extends MetricsPanelCtrl {
   }
   }
 
 
   onInitEditMode() {
   onInitEditMode() {
+    this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
     this.addEditorTab('Axes', axesEditorComponent, 2);
     this.addEditorTab('Axes', axesEditorComponent, 2);
     this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
     this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
-    this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
 
 
     if (config.alertingEnabled) {
     if (config.alertingEnabled) {
       this.addEditorTab('Alert', alertTab, 5);
       this.addEditorTab('Alert', alertTab, 5);

+ 2 - 2
public/app/plugins/panel/graph/series_overrides_ctrl.ts

@@ -1,5 +1,5 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 
 
 /** @ngInject */
 /** @ngInject */
 export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
 export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
@@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
   $scope.updateCurrentOverrides();
   $scope.updateCurrentOverrides();
 }
 }
 
 
-angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl);
+coreModule.controller('SeriesOverridesCtrl', SeriesOverridesCtrl);

+ 5 - 0
public/app/plugins/panel/graph2/README.md

@@ -0,0 +1,5 @@
+# Text Panel -  Native Plugin
+
+The Text Panel is **included** with Grafana.
+
+The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more.

+ 26 - 0
public/app/plugins/panel/graph2/img/icn-text-panel.svg

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<rect style="opacity:0.2;fill:#414042;" width="100" height="100"/>
+<g>
+	<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="50" y1="88.2189" x2="50" y2="11.7811">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0.0595" style="stop-color:#FFE029"/>
+		<stop  offset="0.1303" style="stop-color:#FFD218"/>
+		<stop  offset="0.2032" style="stop-color:#FEC90F"/>
+		<stop  offset="0.2809" style="stop-color:#FDC70C"/>
+		<stop  offset="0.6685" style="stop-color:#F3903F"/>
+		<stop  offset="0.8876" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_1_);" d="M15.107,30.157h-2.593l0.395-18.376h74.183l0.395,18.376h-2.424
+		c-0.865-5.035-2.049-8.671-3.551-10.908c-1.504-2.235-3.12-3.607-4.848-4.115c-1.729-0.507-4.679-0.761-8.85-0.761H55.524V68.32
+		c0,5.975,0.141,9.903,0.423,11.781c0.282,1.88,1.043,3.27,2.283,4.171c1.24,0.902,3.57,1.353,6.99,1.353h3.72v2.593H30.834v-2.593
+		h3.946c3.269,0,5.533-0.413,6.793-1.24c1.258-0.826,2.066-2.114,2.424-3.861c0.357-1.747,0.535-5.815,0.535-12.204V14.374h-11.33
+		c-4.924,0-8.23,0.235-9.921,0.704c-1.691,0.471-3.279,1.888-4.764,4.256C17.032,21.702,15.896,25.31,15.107,30.157z"/>
+</g>
+<g>
+	<path style="fill:#898989;" d="M99,1v98H1V1H99 M100,0H0v100h100V0L100,0z"/>
+</g>
+</svg>

+ 43 - 0
public/app/plugins/panel/graph2/module.tsx

@@ -0,0 +1,43 @@
+// Libraries
+import _ from 'lodash';
+import React, { PureComponent } from 'react';
+
+// Components
+import Graph from 'app/viz/Graph';
+import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
+
+// Types
+import { PanelProps, NullValueMode } from 'app/types';
+
+interface Options {
+  showBars: boolean;
+}
+
+interface Props extends PanelProps {
+  options: Options;
+}
+
+export class Graph2 extends PureComponent<Props> {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    const { timeSeries, timeRange } = this.props;
+
+    const vmSeries = getTimeSeriesVMs({
+      timeSeries: timeSeries,
+      nullValueMode: NullValueMode.Ignore,
+    });
+
+    return <Graph timeSeries={vmSeries} timeRange={timeRange} />;
+  }
+}
+
+export class TextOptions extends PureComponent<any> {
+  render() {
+    return <p>Text2 Options component</p>;
+  }
+}
+
+export { Graph2 as PanelComponent, TextOptions as PanelOptions };

+ 17 - 0
public/app/plugins/panel/graph2/plugin.json

@@ -0,0 +1,17 @@
+{
+  "type": "panel",
+  "name": "React Graph",
+  "id": "graph2",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icn-text-panel.svg",
+      "large": "img/icn-text-panel.svg"
+    }
+  }
+}
+

+ 3 - 5
public/app/plugins/panel/heatmap/color_legend.ts

@@ -1,12 +1,10 @@
-import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
 import * as d3 from 'd3';
 import * as d3 from 'd3';
 import { contextSrv } from 'app/core/core';
 import { contextSrv } from 'app/core/core';
 import { tickStep } from 'app/core/utils/ticks';
 import { tickStep } from 'app/core/utils/ticks';
 import { getColorScale, getOpacityScale } from './color_scale';
 import { getColorScale, getOpacityScale } from './color_scale';
-
-const module = angular.module('grafana.directives');
+import coreModule from 'app/core/core_module';
 
 
 const LEGEND_HEIGHT_PX = 6;
 const LEGEND_HEIGHT_PX = 6;
 const LEGEND_WIDTH_PX = 100;
 const LEGEND_WIDTH_PX = 100;
@@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0;
 /**
 /**
  * Color legend for heatmap editor.
  * Color legend for heatmap editor.
  */
  */
-module.directive('colorLegend', () => {
+coreModule.directive('colorLegend', () => {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
     template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
     template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
@@ -52,7 +50,7 @@ module.directive('colorLegend', () => {
 /**
 /**
  * Heatmap legend with scale values.
  * Heatmap legend with scale values.
  */
  */
-module.directive('heatmapLegend', () => {
+coreModule.directive('heatmapLegend', () => {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
     template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
     template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,

+ 5 - 0
public/app/plugins/panel/text2/README.md

@@ -0,0 +1,5 @@
+# Text Panel -  Native Plugin
+
+The Text Panel is **included** with Grafana.
+
+The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more.

+ 186 - 0
public/app/plugins/panel/text2/img/icn-graph-panel.svg

@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<polyline style="fill:none;stroke:#898989;stroke-width:2;stroke-miterlimit:10;" points="4.734,34.349 36.05,19.26 64.876,36.751 
+	96.308,6.946 "/>
+<circle style="fill:#898989;" cx="4.885" cy="33.929" r="4.885"/>
+<circle style="fill:#898989;" cx="35.95" cy="19.545" r="4.885"/>
+<circle style="fill:#898989;" cx="65.047" cy="36.046" r="4.885"/>
+<circle style="fill:#898989;" cx="94.955" cy="7.135" r="4.885"/>
+<g>
+	<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="5" y1="103.7019" x2="5" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_1_);" d="M9.001,48.173H0.999C0.447,48.173,0,48.62,0,49.172V100h10V49.172
+		C10,48.62,9.553,48.173,9.001,48.173z"/>
+	<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5" y1="98.9423" x2="5" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_2_);" d="M0,69.173v30.563h10V69.173"/>
+	<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="5" y1="99.4343" x2="5" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_3_);" d="M0,83.166v16.701h10V83.166"/>
+</g>
+<g>
+	<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="20" y1="103.7019" x2="20" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_4_);" d="M24.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
+		C25,41.216,24.553,40.769,24.001,40.769z"/>
+	<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="20" y1="98.9423" x2="20" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_5_);" d="M15,64.716v35.02h10v-35.02"/>
+	<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="20" y1="99.4343" x2="20" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_6_);" d="M15,80.731v19.137h10V80.731"/>
+</g>
+<g>
+	<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="35" y1="103.7019" x2="35" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_7_);" d="M39.001,34.423h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V35.422
+		C40,34.87,39.553,34.423,39.001,34.423z"/>
+	<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="35" y1="98.9423" x2="35" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_8_);" d="M30,60.895v38.84h10v-38.84"/>
+	<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="35" y1="99.4343" x2="35" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_9_);" d="M30,78.643v21.225h10V78.643"/>
+</g>
+<g>
+	<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="50" y1="103.7019" x2="50" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_10_);" d="M54.001,41.827h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V42.826
+		C55,42.274,54.553,41.827,54.001,41.827z"/>
+	<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="50" y1="98.9423" x2="50" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_11_);" d="M45,65.352v34.383h10V65.352"/>
+	<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="50" y1="99.4343" x2="50" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_12_);" d="M45,81.079v18.789h10V81.079"/>
+</g>
+<g>
+	<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="65" y1="103.8575" x2="65" y2="29.1875">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_13_);" d="M69.001,50.404h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V51.403
+		C70,50.851,69.553,50.404,69.001,50.404z"/>
+	<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="65" y1="98.8979" x2="65" y2="51.2298">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_14_);" d="M60,70.531v29.193h10V70.531"/>
+	<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="65" y1="99.4105" x2="65" y2="73.3619">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_15_);" d="M60,83.909v15.953h10V83.909"/>
+</g>
+<g>
+	<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="80" y1="104.4108" x2="80" y2="19.0293">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_16_);" d="M84.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
+		C85,41.216,84.553,40.769,84.001,40.769z"/>
+	<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="80" y1="98.9423" x2="80" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_17_);" d="M75,64.716v35.02h10v-35.02"/>
+	<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="80" y1="99.4343" x2="80" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_18_);" d="M75,80.731v19.137h10V80.731"/>
+</g>
+<g>
+	<linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="95" y1="103.5838" x2="95" y2="34.2115">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_19_);" d="M99.001,21.157h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V22.156
+		C100,21.604,99.553,21.157,99.001,21.157z"/>
+	<linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="95" y1="98.9761" x2="95" y2="54.69">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_20_);" d="M90,52.898v46.846h10V52.898"/>
+	<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="95" y1="99.4524" x2="95" y2="75.2518">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_21_);" d="M90,74.272v25.6h10v-25.6"/>
+</g>
+</svg>

+ 14 - 0
public/app/plugins/panel/text2/module.tsx

@@ -0,0 +1,14 @@
+import React, { PureComponent } from 'react';
+import { PanelProps } from 'app/types';
+
+export class Text2 extends PureComponent<PanelProps> {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    return <h2>Text Panel!</h2>;
+  }
+}
+
+export { Text2 as PanelComponent };

+ 19 - 0
public/app/plugins/panel/text2/plugin.json

@@ -0,0 +1,19 @@
+{
+  "type": "panel",
+  "name": "Text v2",
+  "id": "text2",
+
+  "state": "alpha",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icn-graph-panel.svg",
+      "large": "img/icn-graph-panel.svg"
+    }
+  }
+}
+

+ 7 - 3
public/app/core/components/grafana_app.ts → public/app/routes/GrafanaCtrl.ts

@@ -8,9 +8,10 @@ import appEvents from 'app/core/app_events';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
 import colors from 'app/core/utils/colors';
 import colors from 'app/core/utils/colors';
 import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
 import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
-import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
-import { configureStore } from 'app/store/configureStore';
+import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
+import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
 import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
+import { configureStore } from 'app/store/configureStore';
 
 
 export class GrafanaCtrl {
 export class GrafanaCtrl {
   /** @ngInject */
   /** @ngInject */
@@ -23,12 +24,15 @@ export class GrafanaCtrl {
     contextSrv,
     contextSrv,
     bridgeSrv,
     bridgeSrv,
     backendSrv: BackendSrv,
     backendSrv: BackendSrv,
+    timeSrv: TimeSrv,
     datasourceSrv: DatasourceSrv,
     datasourceSrv: DatasourceSrv,
     angularLoader: AngularLoader
     angularLoader: AngularLoader
   ) {
   ) {
-    // sets singleston instances for angular services so react components can access them
+    // make angular loader service available to react components
     setAngularLoader(angularLoader);
     setAngularLoader(angularLoader);
     setBackendSrv(backendSrv);
     setBackendSrv(backendSrv);
+    setDatasourceSrv(datasourceSrv);
+    setTimeSrv(timeSrv);
     configureStore();
     configureStore();
 
 
     $scope.init = () => {
     $scope.init = () => {

+ 24 - 0
public/app/types/index.ts

@@ -8,6 +8,19 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { Invitee, OrgUser, User, UsersState } from './user';
 import { Invitee, OrgUser, User, UsersState } from './user';
 import { DataSource, DataSourcesState } from './datasources';
 import { DataSource, DataSourcesState } from './datasources';
+import {
+  TimeRange,
+  LoadingState,
+  TimeSeries,
+  TimeSeriesVM,
+  TimeSeriesVMs,
+  TimeSeriesStats,
+  NullValueMode,
+  DataQuery,
+  DataQueryResponse,
+  DataQueryOptions,
+} from './series';
+import { PanelProps } from './panel';
 import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
 import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
 
 
 export {
 export {
@@ -45,6 +58,17 @@ export {
   OrgUser,
   OrgUser,
   User,
   User,
   UsersState,
   UsersState,
+  TimeRange,
+  LoadingState,
+  PanelProps,
+  TimeSeries,
+  TimeSeriesVM,
+  TimeSeriesVMs,
+  NullValueMode,
+  TimeSeriesStats,
+  DataQuery,
+  DataQueryResponse,
+  DataQueryOptions,
   PluginDashboard,
   PluginDashboard,
 };
 };
 
 

+ 1 - 0
public/app/types/location.ts

@@ -2,6 +2,7 @@ export interface LocationUpdate {
   path?: string;
   path?: string;
   query?: UrlQueryMap;
   query?: UrlQueryMap;
   routeParams?: UrlQueryMap;
   routeParams?: UrlQueryMap;
+  partial?: boolean;
 }
 }
 
 
 export interface LocationState {
 export interface LocationState {

+ 7 - 0
public/app/types/panel.ts

@@ -0,0 +1,7 @@
+import { LoadingState, TimeSeries, TimeRange } from './series';
+
+export interface PanelProps {
+  timeSeries: TimeSeries[];
+  timeRange: TimeRange;
+  loading: LoadingState;
+}

+ 22 - 0
public/app/types/plugins.ts

@@ -1,3 +1,25 @@
+export interface PluginExports {
+  PanelCtrl?;
+  PanelComponent?: any;
+  Datasource?: any;
+  QueryCtrl?: any;
+  ConfigCtrl?: any;
+  AnnotationsQueryCtrl?: any;
+  PanelOptions?: any;
+}
+
+export interface PanelPlugin {
+  id: string;
+  name: string;
+  meta: any;
+  hideFromList: boolean;
+  module: string;
+  baseUrl: string;
+  info: any;
+  sort: number;
+  exports?: PluginExports;
+}
+
 export interface PluginMeta {
 export interface PluginMeta {
   id: string;
   id: string;
   name: string;
   name: string;

+ 91 - 0
public/app/types/series.ts

@@ -0,0 +1,91 @@
+import { Moment } from 'moment';
+
+export enum LoadingState {
+  NotStarted = 'NotStarted',
+  Loading = 'Loading',
+  Done = 'Done',
+  Error = 'Error',
+}
+
+export interface RawTimeRange {
+  from: Moment | string;
+  to: Moment | string;
+}
+
+export interface TimeRange {
+  from: Moment;
+  to: Moment;
+  raw: RawTimeRange;
+}
+
+export type TimeSeriesValue = string | number | null;
+
+export type TimeSeriesPoints = TimeSeriesValue[][];
+
+export interface TimeSeries {
+  target: string;
+  datapoints: TimeSeriesPoints;
+  unit?: string;
+}
+
+/** View model projection of a time series */
+export interface TimeSeriesVM {
+  label: string;
+  color: string;
+  data: TimeSeriesValue[][];
+  stats: TimeSeriesStats;
+}
+
+export interface TimeSeriesStats {
+  total: number;
+  max: number;
+  min: number;
+  logmin: number;
+  avg: number | null;
+  current: number | null;
+  first: number | null;
+  delta: number;
+  diff: number | null;
+  range: number | null;
+  timeStep: number;
+  count: number;
+  allIsNull: boolean;
+  allIsZero: boolean;
+}
+
+export enum NullValueMode {
+  Null = 'null',
+  Ignore = 'connected',
+  AsZero = 'null as zero',
+}
+
+/** View model projection of many time series */
+export interface TimeSeriesVMs {
+  [index: number]: TimeSeriesVM;
+}
+
+export interface DataQueryResponse {
+  data: TimeSeries[];
+}
+
+export interface DataQuery {
+  refId: string;
+}
+
+export interface DataQueryOptions {
+  timezone: string;
+  range: TimeRange;
+  rangeRaw: RawTimeRange;
+  targets: DataQuery[];
+  panelId: number;
+  dashboardId: number;
+  cacheTimeout?: string;
+  interval: string;
+  intervalMs: number;
+  maxDataPoints: number;
+  scopedVars: object;
+}
+
+export interface DataSourceApi {
+  query(options: DataQueryOptions): Promise<DataQueryResponse>;
+}

Some files were not shown because too many files changed in this diff