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:
       - checkout
       - 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:
           name: sign packages
           command: './scripts/build/sign_packages.sh'
@@ -254,6 +263,53 @@ jobs:
           paths:
             - 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:
     docker:
       - image: circleci/python:2.7-stretch
@@ -267,6 +323,19 @@ jobs:
           name: deploy to s3
           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:
     docker:
       - image: circleci/python:2.7-stretch
@@ -313,7 +382,7 @@ workflows:
     jobs:
       - build-all:
           filters: *filter-only-master
-      - build-enterprise:
+      - build-all-enterprise:
           filters: *filter-only-master
       - codespell:
           filters: *filter-only-master
@@ -356,13 +425,15 @@ workflows:
             - gometalinter
             - mysql-integration-test
             - postgres-integration-test
-            - build-enterprise
+            - build-all-enterprise
           filters: *filter-only-master
 
   release:
     jobs:
       - build-all:
           filters: *filter-only-release
+      - build-all-enterprise:
+          filters: *filter-only-release
       - codespell:
           filters: *filter-only-release
       - gometalinter:
@@ -385,6 +456,17 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           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:
           requires:
             - 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)
 * **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)
+* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
 
 ### Minor
 
@@ -19,8 +20,10 @@
 
 # 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)
 * **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)
 
 # 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
 server_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
 
-### 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.
 
@@ -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" >}}
 
+### 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
 
 {{< 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)
 
-Here is a provisioning example for this datasource.
+Here is a provisioning example using the JWT (Service Account key file) authentication type.
 
 ```yaml
 apiVersion: 1
@@ -206,6 +218,8 @@ datasources:
     jsonData:
       tokenUri: https://oauth2.googleapis.com/token
       clientEmail: stackdriver@myproject.iam.gserviceaccount.com
+      authenticationType: jwt
+      defaultProject: my-project-name
     secureJsonData:
       privateKey: |
         -----BEGIN PRIVATE KEY-----
@@ -214,3 +228,16 @@ datasources:
         yA+23427282348234=
         -----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
 wget <debian package url>
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.1.4_amd64.deb
+sudo dpkg -i grafana_<version>_amd64.deb
 ```
 
 Example:

+ 1 - 1
pkg/api/api.go

@@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
 			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
 		}, 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)
 

+ 7 - 3
pkg/api/frontendsettings.go

@@ -11,7 +11,7 @@ import (
 	"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)
 
 	if c.OrgId != 0 {
@@ -133,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
+		if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
+			continue
+		}
+
 		panels[panel.Id] = map[string]interface{}{
 			"module":       panel.Module,
 			"baseUrl":      panel.BaseUrl,
@@ -196,8 +200,8 @@ func getPanelSort(id string) int {
 	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 {
 		c.JsonApiErr(400, "Failed to get frontend settings", err)
 		return

+ 1 - 1
pkg/api/index.go

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

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

@@ -12,6 +12,7 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"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
@@ -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 {
 			logger.Error("Failed to get access token", "error", err)
 		} else {
 			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) {

+ 5 - 0
pkg/setting/setting.go

@@ -213,6 +213,8 @@ type Cfg struct {
 	TempDataLifetime time.Duration
 
 	MetricsEndpointEnabled bool
+
+	EnableAlphaPanels bool
 }
 
 type CommandLineArgs struct {
@@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	explore := iniFile.Section("explore")
 	ExploreEnabled = explore.Key("enabled").MustBool(false)
 
+	panels := iniFile.Section("panels")
+	cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
+
 	cfg.readSessionConfig()
 	cfg.readSmtpSettings()
 	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"
 
 	"golang.org/x/net/context/ctxhttp"
+	"golang.org/x/oauth2/google"
 
 	"github.com/grafana/grafana/pkg/api/pluginproxy"
 	"github.com/grafana/grafana/pkg/components/null"
@@ -34,6 +35,11 @@ var (
 	metricNameFormat *regexp.Regexp
 )
 
+const (
+	gceAuthentication string = "gce"
+	jwtAuthentication string = "jwt"
+)
+
 // StackdriverExecutor executes queries for the Stackdriver datasource
 type StackdriverExecutor struct {
 	httpClient *http.Client
@@ -71,6 +77,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour
 	switch queryType {
 	case "annotationQuery":
 		result, err = e.executeAnnotationQuery(ctx, tsdbQuery)
+	case "ensureDefaultProjectQuery":
+		result, err = e.ensureDefaultProject(ctx, tsdbQuery)
 	case "timeSeriesQuery":
 		fallthrough
 	default:
@@ -85,6 +93,16 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu
 		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)
 	if err != nil {
 		return nil, err
@@ -550,8 +568,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
 	if !ok {
 		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
 	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)
 
 	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;
 };
 
-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
 const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
@@ -109,39 +113,26 @@ export class GrafanaApp {
       '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
-    this.useModule(coreModule);
+    _.each(angularModules, m => {
+      this.useModule(m);
+    });
 
     // register react angular wrappers
     coreModule.config(setupAngularRoutes);
     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();
       const scroller = elem;
 
+      console.log('scroll');
       if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
         scrollRoot = scroller;
       }

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

@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import { PanelPlugin } from 'app/types/plugins';
 
 export interface BuildInfo {
   version: string;
@@ -9,7 +10,7 @@ export interface BuildInfo {
 
 export class Settings {
   datasources: any;
-  panels: any;
+  panels: PanelPlugin[];
   appSubUrl: string;
   windowTitlePrefix: string;
   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 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/ng_react';
 
-import { grafanaAppDirective } from './components/grafana_app';
 import { searchDirective } from './components/search/search';
 import { infoPopover } from './components/info_popover';
 import { navbarDirective } from './components/navbar/navbar';
@@ -60,7 +59,6 @@ export {
   registerAngularDirectives,
   arrayJoin,
   coreModule,
-  grafanaAppDirective,
   navbarDirective,
   searchDirective,
   liveSrv,

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

@@ -1,2 +1,18 @@
 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';
 
 /** @ngInject */
-export function dashClass() {
+function dashClass($timeout) {
   return {
     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 => {
         if (newValue) {

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

@@ -1,6 +1,7 @@
 import { Action } from 'app/core/actions/location';
 import { LocationState } from 'app/types';
 import { renderUrl } from 'app/core/utils/url';
+import _ from 'lodash';
 
 export const initialState: LocationState = {
   url: '',
@@ -12,11 +13,17 @@ export const initialState: LocationState = {
 export const locationReducer = (state = initialState, action: Action): LocationState => {
   switch (action.type) {
     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 {
         url: renderUrl(path || state.path, query),
         path: path || state.path,
-        query: query || state.query,
+        query: query,
         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 {
   /** @ngInject */
-  constructor(private $compile, private $rootScope) {}
+  constructor(private $compile) {}
 
   addDirective(element, name, scope) {
     const child = angular.element(document.createElement(name));
@@ -14,25 +14,19 @@ class DynamicDirectiveSrv {
   }
 
   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) {

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

@@ -148,7 +148,7 @@ export class KeybindingSrv {
     this.bind('mod+o', () => {
       dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
       appEvents.emit('graph-hover-clear');
-      this.$rootScope.$broadcast('refresh');
+      dashboard.startRefresh();
     });
 
     this.bind('mod+s', e => {
@@ -257,7 +257,7 @@ export class KeybindingSrv {
     });
 
     this.bind('d r', () => {
-      this.$rootScope.$broadcast('refresh');
+      dashboard.startRefresh();
     });
 
     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 './repeat_option/repeat_option';
 import './dashgrid/DashboardGridDirective';
-import './dashgrid/PanelLoader';
 import './dashgrid/RowOptions';
 import './folder_picker/folder_picker';
 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 coreModule from 'app/core/core_module';
-import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
 
-export class DashboardCtrl implements PanelContainer {
+export class DashboardCtrl {
   dashboard: DashboardModel;
   dashboardViewState: any;
   loadedFallbackDashboard: boolean;
@@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer {
     private dashboardSrv,
     private unsavedChangesSrv,
     private dashboardViewStateSrv,
-    public playlistSrv,
-    private panelLoader
+    public playlistSrv
   ) {
     // temp hack due to way dashboards are loaded
     // can't use controllerAs on route yet
@@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer {
     return this.dashboard;
   }
 
-  getPanelLoader() {
-    return this.panelLoader;
-  }
-
-  timezoneChanged() {
-    this.$rootScope.$broadcast('refresh');
-  }
-
   getPanelContainer() {
     return this;
   }
@@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer {
     this.dashboard.removePanel(panel);
   }
 
+  onDestroy() {
+    if (this.dashboard) {
+      this.dashboard.destroy();
+    }
+  }
+
   init(dashboard) {
     this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
     this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
     this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
+    this.$scope.$on('$destroy', this.onDestroy.bind(this));
     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);
   }
 
+  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) {
     if (!data) {
       data = {};

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

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

+ 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 { DashboardPanel } from './DashboardPanel';
 import { DashboardModel } from '../dashboard_model';
-import { PanelContainer } from './PanelContainer';
 import { PanelModel } from '../panel_model';
 import classNames from 'classnames';
 import sizeMe from 'react-sizeme';
@@ -60,18 +59,15 @@ function GridWrapper({
 const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
 
 export interface DashboardGridProps {
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 
 export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   gridToPanelMap: any;
-  panelContainer: PanelContainer;
-  dashboard: DashboardModel;
   panelMap: { [id: string]: PanelModel };
 
   constructor(props) {
     super(props);
-    this.panelContainer = this.props.getPanelContainer();
     this.onLayoutChange = this.onLayoutChange.bind(this);
     this.onResize = this.onResize.bind(this);
     this.onResizeStop = this.onResizeStop.bind(this);
@@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
     this.state = { animated: false };
 
     // 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() {
     const layout = [];
     this.panelMap = {};
 
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       const stringId = panel.id.toString();
       this.panelMap[stringId] = panel;
 
@@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
       this.panelMap[newPos.i].updateGridPos(newPos);
     }
 
-    this.dashboard.sortPanelsByGridPos();
+    this.props.dashboard.sortPanelsByGridPos();
   }
 
   triggerForceUpdate() {
@@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   }
 
   onWidthChange() {
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       panel.resizeDone();
     }
   }
 
+  onViewModeChanged(payload) {
+    this.setState({ animated: !payload.fullscreen });
+  }
+
   updateGridPos(item, layout) {
     this.panelMap[item.i].updateGridPos(item);
 
@@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
 
   componentDidMount() {
     setTimeout(() => {
-      this.setState(() => {
-        return { animated: true };
-      });
+      this.setState({ animated: true });
     });
   }
 
   renderPanels() {
     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 });
       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>
       );
     }
@@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
       <SizedReactLayoutGrid
         className={classNames({ layout: true, animated: this.state.animated })}
         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}
         onWidthChange={this.onWidthChange}
         onDragStop={this.onDragStop}

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

@@ -1,6 +1,4 @@
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 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 {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;
-  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;
-  attachedPanel: AttachedPanel;
+  angularPanel: AngularComponent;
+  pluginInfo: any;
+  specialPanels = {};
 
   constructor(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() {
-    if (!this.element) {
+    this.loadPlugin();
+  }
+
+  componentDidUpdate() {
+    this.loadPlugin();
+
+    // handle angular plugin loading
+    if (!this.element || this.angularPanel) {
       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() {
-    // 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 classNames from 'classnames';
 import { PanelModel } from '../panel_model';
-import { PanelContainer } from './PanelContainer';
+import { DashboardModel } from '../dashboard_model';
 import templateSrv from 'app/features/templating/template_srv';
 import appEvents from 'app/core/app_events';
 
 export interface DashboardRowProps {
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 
 export class DashboardRow extends React.Component<DashboardRowProps, any> {
-  dashboard: any;
-  panelContainer: any;
-
   constructor(props) {
     super(props);
 
@@ -21,9 +18,6 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       collapsed: this.props.panel.collapsed,
     };
 
-    this.panelContainer = this.props.getPanelContainer();
-    this.dashboard = this.panelContainer.getDashboard();
-
     this.toggle = this.toggle.bind(this);
     this.openSettings = this.openSettings.bind(this);
     this.delete = this.delete.bind(this);
@@ -31,7 +25,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
 
   toggle() {
-    this.dashboard.toggleRow(this.props.panel);
+    this.props.dashboard.toggleRow(this.props.panel);
 
     this.setState(prevState => {
       return { collapsed: !prevState.collapsed };
@@ -39,7 +33,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
 
   update() {
-    this.dashboard.processRepeats();
+    this.props.dashboard.processRepeats();
     this.forceUpdate();
   }
 
@@ -61,14 +55,10 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       altActionText: 'Delete row only',
       icon: 'fa-trash',
       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: () => {
-        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 count = this.props.panel.panels ? this.props.panel.panels.length : 0;
     const panels = count === 1 ? 'panel' : 'panels';
-    const canEdit = this.dashboard.meta.canEdit === true;
+    const canEdit = this.props.dashboard.meta.canEdit === true;
 
     return (
       <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) {
       delete search.fullscreen;
       delete search.edit;
+      delete search.tab;
+      delete search.panelId;
     }
     this.$location.search(search);
   }

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

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

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

@@ -32,7 +32,7 @@ export class SettingsCtrl {
 
     this.$scope.$on('$destroy', () => {
       this.dashboard.updateSubmenuVisibility();
-      this.$rootScope.$broadcast('refresh');
+      this.dashboard.startRefresh();
       setTimeout(() => {
         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.snapshot.external = external;
-
-      $rootScope.$broadcast('refresh');
+      $scope.dashboard.startRefresh();
 
       $timeout(() => {
         $scope.saveSnapshot(external);

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

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

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

@@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow';
 import { PanelModel } from '../panel_model';
 
 describe('DashboardRow', () => {
-  let wrapper, panel, getPanelContainer, dashboardMock;
+  let wrapper, panel, dashboardMock;
 
   beforeEach(() => {
     dashboardMock = {
@@ -14,13 +14,8 @@ describe('DashboardRow', () => {
       },
     };
 
-    getPanelContainer = jest.fn().mockReturnValue({
-      getDashboard: jest.fn().mockReturnValue(dashboardMock),
-      getPanelLoader: jest.fn(),
-    });
-
     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', () => {
@@ -41,14 +36,14 @@ describe('DashboardRow', () => {
 
   it('should not show row drag handle when cannot edit', () => {
     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);
   });
 
   it('should have zero actions when cannot edit', () => {
     dashboardMock.meta.canEdit = 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);
   });
 });

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

@@ -240,5 +240,5 @@ stubs['-- Grafana --'] = {
 };
 
 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 config from 'app/core/config';
 import { DashboardViewState } from '../view_state_srv';
+import { DashboardModel } from '../dashboard_model';
 
 describe('when updating view state', () => {
   const location = {
@@ -10,14 +11,13 @@ describe('when updating view state', () => {
   };
 
   const $scope = {
+    appEvent: jest.fn(),
     onAppEvent: jest.fn(() => {}),
-    dashboard: {
-      meta: {},
-      panels: [],
-    },
+    dashboard: new DashboardModel({
+      panels: [{ id: 1 }],
+    }),
   };
 
-  const $rootScope = {};
   let viewState;
 
   beforeEach(() => {
@@ -33,7 +33,7 @@ describe('when updating view state', () => {
       location.search = jest.fn(() => {
         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', () => {
@@ -55,7 +55,7 @@ describe('when updating view state', () => {
 
   describe('to fullscreen false', () => {
     beforeEach(() => {
-      viewState = new DashboardViewState($scope, location, {}, $rootScope);
+      viewState = new DashboardViewState($scope, location, {});
     });
     it('should remove params from query string', () => {
       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;
 
   /** @ngInject */
-  constructor(private $rootScope, private variableSrv, private $location) {
+  constructor(private variableSrv, private $location) {
     this.annotations = this.dashboard.templating.list;
     this.variables = this.variableSrv.variables;
   }
 
   annotationStateChanged() {
-    this.$rootScope.$broadcast('refresh');
+    this.dashboard.startRefresh();
   }
 
   variableUpdated(variable) {

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

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

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

@@ -5,7 +5,7 @@
 		<div class="gf-form">
 			<label class="gf-form-label width-10">Timezone</label>
 			<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>
 

+ 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-backward', () => this.move(-1), $scope);
-    $rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
     $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
 
+    this.dashboard.on('refresh', this.onRefresh.bind(this), $scope);
+
     // init options
     this.panel = this.dashboard.timepicker;
     _.defaults(this.panel, TimePickerCtrl.defaults);

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

@@ -1,6 +1,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 import config from 'app/core/config';
+import appEvents from 'app/core/app_events';
 import { DashboardModel } from './dashboard_model';
 
 // represents the transient view state
@@ -10,12 +11,11 @@ export class DashboardViewState {
   panelScopes: any;
   $scope: any;
   dashboard: DashboardModel;
-  editStateChanged: any;
   fullscreenPanel: any;
   oldTimeRange: any;
 
   /** @ngInject */
-  constructor($scope, private $location, private $timeout, private $rootScope) {
+  constructor($scope, private $location, private $timeout) {
     const self = this;
     self.state = {};
     self.panelScopes = [];
@@ -33,10 +33,6 @@ export class DashboardViewState {
       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
     // don't want url changes like adding orgId to add browser history
     $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);
     this.dashboard.meta.fullscreen = this.state.fullscreen;
 
@@ -124,110 +117,59 @@ export class DashboardViewState {
   }
 
   syncState() {
-    if (this.panelScopes.length === 0) {
-      return;
-    }
-
     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) {
-      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(() => {
-      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 {
-        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()
     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 */
-export function dashboardViewStateSrv($location, $timeout, $rootScope) {
+export function dashboardViewStateSrv($location, $timeout) {
   return {
     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 dateMath from 'app/core/utils/datemath';
 import { getExploreUrl } from 'app/core/utils/explore';
-
 import { metricsTabDirective } from './metrics_tab';
 
 class MetricsPanelCtrl extends PanelCtrl {
   scope: any;
   datasource: any;
-  datasourceName: any;
   $q: any;
   $timeout: any;
   contextSrv: any;
@@ -45,10 +43,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.scope = $scope;
     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('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this));
     this.events.on('panel-teardown', this.onPanelTearDown.bind(this));
@@ -62,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
 
   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');
   }
 
@@ -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() {
     const items = [];
     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';
 
+// 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 {
   dsName: string;
   panel: any;
@@ -24,6 +32,9 @@ export class MetricsTabCtrl {
     $scope.ctrl = this;
 
     this.panel = this.panelCtrl.panel;
+    this.panel.datasource = this.panel.datasource || null;
+    this.panel.targets = this.panel.targets || [{}];
+
     this.dashboard = this.panelCtrl.dashboard;
     this.datasources = datasourceSrv.getMetricSources();
     this.panelDsValue = this.panelCtrl.panel.datasource;
@@ -66,10 +77,29 @@ export class MetricsTabCtrl {
     }
 
     this.datasourceInstance = option.datasource;
-    this.panelCtrl.setDatasource(option.datasource);
+    this.setDatasource(option.datasource);
     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) {
     if (!option) {
       return;
@@ -120,3 +150,5 @@ export function metricsTabDirective() {
     controller: MetricsTabCtrl,
   };
 }
+
+coreModule.directive('metricsTab', metricsTabDirective);

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

@@ -24,10 +24,8 @@ export class PanelCtrl {
   $injector: any;
   $location: any;
   $timeout: any;
-  fullscreen: boolean;
   inspector: any;
   editModeInitiated: boolean;
-  editMode: any;
   height: any;
   containerHeight: any;
   events: Emitter;
@@ -49,7 +47,6 @@ export class PanelCtrl {
       this.pluginName = plugin.name;
     }
 
-    $scope.$on('refresh', () => this.refresh());
     $scope.$on('component-did-mount', () => this.panelDidMount());
 
     $scope.$on('$destroy', () => {
@@ -58,13 +55,9 @@ export class PanelCtrl {
     });
   }
 
-  init() {
-    this.events.emit('panel-initialized');
-    this.publishAppEvent('panel-initialized', { scope: this.$scope });
-  }
-
   panelDidMount() {
     this.events.emit('component-did-mount');
+    this.dashboard.panelInitialized(this.panel);
   }
 
   renderingCompleted() {
@@ -72,7 +65,7 @@ export class PanelCtrl {
   }
 
   refresh() {
-    this.events.emit('refresh', null);
+    this.panel.refresh();
   }
 
   publishAppEvent(evtName, evt) {
@@ -102,6 +95,7 @@ export class PanelCtrl {
   initEditMode() {
     this.editorTabs = [];
     this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
+
     this.editModeInitiated = true;
     this.events.emit('init-edit-mode', null);
 
@@ -122,14 +116,15 @@ export class PanelCtrl {
     route.updateParams();
   }
 
-  addEditorTab(title, directiveFn, index?) {
-    const editorTab = { title, directiveFn };
+  addEditorTab(title, directiveFn, index?, icon?) {
+    const editorTab = { title, directiveFn, icon };
 
     if (_.isString(directiveFn)) {
       editorTab.directiveFn = () => {
         return { templateUrl: directiveFn };
       };
     }
+
     if (index) {
       this.editorTabs.splice(index, 0, editorTab);
     } else {
@@ -190,7 +185,7 @@ export class PanelCtrl {
 
   getExtendedMenu() {
     const menu = [];
-    if (!this.fullscreen && this.dashboard.meta.canEdit) {
+    if (!this.panel.fullscreen && this.dashboard.meta.canEdit) {
       menu.push({
         text: 'Duplicate',
         click: 'ctrl.duplicate()',
@@ -220,15 +215,15 @@ export class PanelCtrl {
   }
 
   otherPanelInFullscreenMode() {
-    return this.dashboard.meta.fullscreen && !this.fullscreen;
+    return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
   }
 
   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);
-      this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
+      this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight;
     } else {
       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();
     }
 
+    // hacky solution
+    if (this.panel.isEditing && !this.editModeInitiated) {
+      this.initEditMode();
+    }
+
     this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT);
   }
 
@@ -247,9 +247,6 @@ export class PanelCtrl {
 
   duplicate() {
     this.dashboard.duplicatePanel(this.panel);
-    this.$timeout(() => {
-      this.$scope.$root.$broadcast('render');
-    });
   }
 
   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 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 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>
@@ -85,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
         ctrl.dashboard.setPanelFocus(0);
       }
 
-      function panelHeightUpdated() {
-        panelContent.css({ height: ctrl.height + 'px' });
-      }
-
       function resizeScrollableContent() {
         if (panelScrollbar) {
           panelScrollbar.update();
@@ -133,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
 
       ctrl.events.on('panel-size-changed', () => {
         ctrl.calculatePanelHeight();
-        panelHeightUpdated();
         $timeout(() => {
           resizeScrollableContent();
           ctrl.render();
@@ -142,7 +142,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
 
       // set initial height
       ctrl.calculatePanelHeight();
-      panelHeightUpdated();
 
       ctrl.events.on('render', () => {
         if (transparentLastState !== ctrl.panel.transparent) {

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

@@ -1,6 +1,7 @@
 import angular from 'angular';
 
 const directiveModule = angular.module('grafana.directives');
+const directiveCache = {};
 
 /** @ngInject */
 function panelEditorTab(dynamicDirectiveSrv) {
@@ -12,17 +13,24 @@ function panelEditorTab(dynamicDirectiveSrv) {
     },
     directive: scope => {
       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="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
     <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>
   </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-inline">
     <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>
-
       <gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
                         lookup-text="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 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 dashListPanel from 'app/plugins/panel/dashlist/module';
 import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
@@ -40,6 +42,8 @@ const builtInPlugins = {
   'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
 
   '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/dashlist/module': dashListPanel,
   'app/plugins/panel/pluginlist/module': pluginsListPanel,

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

@@ -1,8 +1,14 @@
+// Libraries
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
+
+// Utils
 import config from 'app/core/config';
 import { importPluginModule } from './plugin_loader';
 
+// Types
+import { DataSourceApi } from 'app/types/series';
+
 export class DatasourceSrv {
   datasources: any;
 
@@ -15,7 +21,7 @@ export class DatasourceSrv {
     this.datasources = {};
   }
 
-  get(name?) {
+  get(name?): Promise<DataSourceApi> {
     if (!name) {
       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);
 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';
 
 /** @ngInject */
-function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
+function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
   function getTemplate(component) {
     if (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.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;
       });
 
@@ -207,10 +207,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
 
     // let a binding digest cycle complete before adding to dom
     setTimeout(() => {
-      elem.append(child);
       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);
         })
         .catch(err => {
-          $rootScope.appEvent('alert-error', ['Plugin Error', err.message || 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 TableModel from 'app/core/table_model';
 import { coreModule, appEvents, contextSrv } from 'app/core/core';
+import { PluginExports } from 'app/types/plugins';
 import * as datemath from 'app/core/utils/datemath';
 import * as fileExport from 'app/core/utils/file_export';
 import * as flatten from 'app/core/utils/flatten';
@@ -140,11 +141,12 @@ const flotDeps = [
   'jquery.flot.events',
   'jquery.flot.gauge',
 ];
+
 for (const flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });
 }
 
-export function importPluginModule(path: string): Promise<any> {
+export function importPluginModule(path: string): Promise<PluginExports> {
   const builtIn = builtInPlugins[path];
   if (builtIn) {
     return Promise.resolve(builtIn);

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

@@ -1,5 +1,6 @@
 import '../all';
 import { VariableSrv } from '../variable_srv';
+import { DashboardModel } from '../../dashboard/dashboard_model';
 import moment from 'moment';
 import $q from 'q';
 
@@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) {
           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);
         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 { VariableSrv } from '../variable_srv';
+import { DashboardModel } from '../../dashboard/dashboard_model';
 import $q from 'q';
 
 describe('VariableSrv init', function(this: any) {
@@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) {
         ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
 
         ctx.variableSrv.$location.search = () => scenario.urlParams;
-        ctx.variableSrv.dashboard = {
+        ctx.variableSrv.dashboard = new DashboardModel({
           templating: { list: scenario.variables },
-        };
+        });
 
         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 _ from 'lodash';
+
+// Utils & Services
 import coreModule from 'app/core/core_module';
 import { variableTypes } from './variable';
 import { Graph } from 'app/core/utils/dag';
@@ -10,13 +13,12 @@ export class VariableSrv {
 
   /** @ngInject */
   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);
   }
 
   init(dashboard) {
     this.dashboard = dashboard;
+    this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this));
 
     // create working class models representing variables
     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 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) {
@@ -133,7 +133,7 @@ export class VariableSrv {
     return this.$q.all(promises).then(() => {
       if (emitChangeEvents) {
         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">
     </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>
 
-      <dashboard-grid get-panel-container="ctrl.getPanelContainer">
-      </dashboard-grid>
+      <dashboard-grid dashboard="ctrl.dashboard"></dashboard-grid>
     </div>
   </div>
 </div>

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

@@ -137,7 +137,11 @@ export default class CloudWatchDatasource {
       if (res.results) {
         _.forEach(res.results, queryRes => {
           _.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 coreModule from 'app/core/core_module';
 import _ from 'lodash';
 
 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 * 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 * 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 'jquery';
 import rst2html from 'rst2html';
 import Drop from 'tether-drop';
+import coreModule from 'app/core/core_module';
 
 /** @ngInject */
 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) {
   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 'jquery';
 import rst2html from 'rst2html';
+import coreModule from 'app/core/core_module';
 
 /** @ngInject */
 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;
   validationErrors: string[] = [];
   inputDataValid: boolean;
+  authenticationTypes: any[];
+  defaultAuthenticationType: string;
 
   /** @ngInject */
   constructor(datasourceSrv) {
+    this.defaultAuthenticationType = 'jwt';
     this.datasourceSrv = datasourceSrv;
     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.secureJsonFields = this.current.secureJsonFields || {};
+    this.authenticationTypes = [
+      { key: this.defaultAuthenticationType, value: 'Google JWT File' },
+      { key: 'gce', value: 'GCE Default Service Account' },
+    ];
   }
 
   save(jwt) {
@@ -35,6 +45,10 @@ export class StackdriverConfigCtrl {
       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) {
       this.inputDataValid = true;
       return true;
@@ -67,7 +81,7 @@ export class StackdriverConfigCtrl {
     this.inputDataValid = false;
     this.jsonText = '';
 
-    this.current.jsonData = {};
+    this.current.jsonData = Object.assign({}, { authenticationType: this.current.jsonData.authenticationType });
     this.current.secureJsonData = {};
     this.current.secureJsonFields = {};
   }

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

@@ -1,11 +1,14 @@
 import { stackdriverUnitMappings } from './constants';
 import appEvents from 'app/core/app_events';
+import _ from 'lodash';
 
 export default class StackdriverDatasource {
   id: number;
   url: string;
   baseUrl: string;
   projectName: string;
+  authenticationType: string;
+  queryPromise: Promise<any>;
 
   /** @ngInject */
   constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
@@ -14,6 +17,7 @@ export default class StackdriverDatasource {
     this.doRequest = this.doRequest;
     this.id = instanceSettings.id;
     this.projectName = instanceSettings.jsonData.defaultProject || '';
+    this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
   }
 
   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) {
@@ -89,7 +97,7 @@ export default class StackdriverDatasource {
   }
 
   resolvePanelUnitFromTargets(targets: any[]) {
-    let unit = 'none';
+    let unit;
     if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
       if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) {
         unit = stackdriverUnitMappings[targets[0].unit];
@@ -99,28 +107,34 @@ export default class StackdriverDatasource {
   }
 
   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) {
@@ -170,76 +184,84 @@ export default class StackdriverDatasource {
     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) {
-          // 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() {
     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 {
-        throw new Error('No projects found');
+        return this.projectName;
       }
     } 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 {
-      const metricsApiPath = `v3/projects/${projectId}/metricDescriptors`;
+      const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
       const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
 
       const metrics = data.metricDescriptors.map(m => {
@@ -253,7 +275,8 @@ export default class StackdriverDatasource {
 
       return metrics;
     } 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="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>
-      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
       visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
     </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>
+
+    <h5>GCE Default Service Account</h5>
     <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>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
-        the documentation.</a></p>
+        the documentation.</a>
+    </p>
   </div>
 </div>
 
 <div class="gf-form-group">
   <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
       contents will be encrypted and saved in the Grafana database.</info-popover>
   </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">
         <form>
@@ -52,23 +69,23 @@
   </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>
 
   <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" />
   </div>
   <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">
-    <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' />
   </div>
   <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">
   </div>
 
@@ -81,6 +98,8 @@
   </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">
       <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 class="gf-form">
       <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">
     <pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
   </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 />
 

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

@@ -28,10 +28,7 @@
       "method": "GET",
       "url": "https://content-monitoring.googleapis.com",
       "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": {
           "token_uri": "{{.JsonData.tokenUri}}",
           "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 * as options from './constants';
 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 {
   static templateUrl = 'partials/query.editor.html';
   target: {
-    project: {
-      id: string;
-      name: string;
-    };
+    defaultProject: string;
     unit: string;
     metricType: string;
     service: string;
@@ -38,10 +35,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
   defaultServiceValue = 'All Services';
 
   defaults = {
-    project: {
-      id: 'default',
-      name: 'loading project...',
-    },
+    defaultProject: 'loading project...',
     metricType: this.defaultDropdownValue,
     service: this.defaultServiceValue,
     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 { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
 import appEvents from 'app/core/app_events';
@@ -79,12 +79,22 @@ export class StackdriverFilterCtrl {
   }
 
   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() {
-    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.metrics = this.getMetricsList();
       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', () => {
   const instanceSettings = {
     jsonData: {
-      projectName: 'testproject',
+      defaultProject: 'testproject',
     },
   };
   const templateSrv = new TemplateSrvStub();
@@ -53,7 +53,9 @@ describe('StackdriverDataSource', () => {
           datasourceRequest: async () =>
             Promise.reject({
               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);
@@ -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', () => {
     const options = {
       range: {
@@ -235,8 +200,8 @@ describe('StackdriverDataSource', () => {
         beforeEach(() => {
           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', () => {
@@ -262,16 +227,16 @@ describe('StackdriverDataSource', () => {
         beforeEach(() => {
           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', () => {
         beforeEach(() => {
           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 };
       });
   }

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

@@ -1,11 +1,10 @@
-import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
 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 {
     link: (scope, elem) => {
       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() {
+    this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
     this.addEditorTab('Axes', axesEditorComponent, 2);
     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) {
       this.addEditorTab('Alert', alertTab, 5);

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

@@ -1,5 +1,5 @@
 import _ from 'lodash';
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 
 /** @ngInject */
 export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
@@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
   $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 'jquery';
 import * as d3 from 'd3';
 import { contextSrv } from 'app/core/core';
 import { tickStep } from 'app/core/utils/ticks';
 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_WIDTH_PX = 100;
@@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0;
 /**
  * Color legend for heatmap editor.
  */
-module.directive('colorLegend', () => {
+coreModule.directive('colorLegend', () => {
   return {
     restrict: 'E',
     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.
  */
-module.directive('heatmapLegend', () => {
+coreModule.directive('heatmapLegend', () => {
   return {
     restrict: 'E',
     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 colors from 'app/core/utils/colors';
 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 { configureStore } from 'app/store/configureStore';
 
 export class GrafanaCtrl {
   /** @ngInject */
@@ -23,12 +24,15 @@ export class GrafanaCtrl {
     contextSrv,
     bridgeSrv,
     backendSrv: BackendSrv,
+    timeSrv: TimeSrv,
     datasourceSrv: DatasourceSrv,
     angularLoader: AngularLoader
   ) {
-    // sets singleston instances for angular services so react components can access them
+    // make angular loader service available to react components
     setAngularLoader(angularLoader);
     setBackendSrv(backendSrv);
+    setDatasourceSrv(datasourceSrv);
+    setTimeSrv(timeSrv);
     configureStore();
 
     $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 { Invitee, OrgUser, User, UsersState } from './user';
 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';
 
 export {
@@ -45,6 +58,17 @@ export {
   OrgUser,
   User,
   UsersState,
+  TimeRange,
+  LoadingState,
+  PanelProps,
+  TimeSeries,
+  TimeSeriesVM,
+  TimeSeriesVMs,
+  NullValueMode,
+  TimeSeriesStats,
+  DataQuery,
+  DataQueryResponse,
+  DataQueryOptions,
   PluginDashboard,
 };
 

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

@@ -2,6 +2,7 @@ export interface LocationUpdate {
   path?: string;
   query?: UrlQueryMap;
   routeParams?: UrlQueryMap;
+  partial?: boolean;
 }
 
 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 {
   id: 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