Procházet zdrojové kódy

Merge branch 'master' into core/theming

Dominik Prokop před 6 roky
rodič
revize
ac50d2b31f
100 změnil soubory, kde provedl 2278 přidání a 1296 odebrání
  1. 5 0
      CHANGELOG.md
  2. 10 2
      Gopkg.lock
  3. 27 0
      devenv/docker/blocks/loki/config.yaml
  4. 2 10
      devenv/docker/blocks/loki/docker-compose.yaml
  5. 19 0
      docs/sources/features/datasources/cloudwatch.md
  6. 58 7
      docs/sources/http_api/annotations.md
  7. 1 3
      docs/sources/installation/configuration.md
  8. 1 0
      docs/sources/reference/templating.md
  9. 1 1
      packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx
  10. 10 0
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx
  11. 25 6
      packages/grafana-ui/src/types/plugin.ts
  12. 59 0
      pkg/api/annotations.go
  13. 62 0
      pkg/api/annotations_test.go
  14. 3 2
      pkg/api/api.go
  15. 1 1
      pkg/api/common_test.go
  16. 8 0
      pkg/api/dtos/annotations.go
  17. 1 1
      pkg/api/login.go
  18. 1 1
      pkg/middleware/middleware_test.go
  19. 15 3
      pkg/services/auth/auth_token.go
  20. 39 0
      pkg/services/auth/auth_token_test.go
  21. 1 4
      pkg/services/sqlstore/sqlstore.go
  22. 3 1
      pkg/tsdb/cloudwatch/cloudwatch.go
  23. 87 1
      pkg/tsdb/cloudwatch/metric_find_query.go
  24. 57 0
      pkg/tsdb/cloudwatch/metric_find_query_test.go
  25. 1 4
      pkg/tsdb/mssql/mssql.go
  26. 0 25
      pkg/util/ip.go
  27. 31 6
      pkg/util/ip_address.go
  28. 83 1
      pkg/util/ip_address_test.go
  29. 0 43
      pkg/util/ip_test.go
  30. 26 11
      public/app/core/services/backend_srv.ts
  31. 30 2
      public/app/core/utils/explore.test.ts
  32. 46 5
      public/app/core/utils/explore.ts
  33. 23 0
      public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx
  34. 61 39
      public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
  35. 42 5
      public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss
  36. 86 0
      public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap
  37. 1 0
      public/app/features/dashboard/components/SaveModals/index.ts
  38. 2 2
      public/app/features/dashboard/panel_editor/PanelEditor.tsx
  39. 1 1
      public/app/features/dashboard/panel_editor/QueriesTab.tsx
  40. 20 1
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  41. 20 25
      public/app/features/explore/Explore.tsx
  42. 8 1
      public/app/features/explore/ExploreToolbar.tsx
  43. 1 0
      public/app/features/explore/Graph.test.tsx
  44. 7 8
      public/app/features/explore/Graph.tsx
  45. 4 2
      public/app/features/explore/GraphContainer.tsx
  46. 1 1
      public/app/features/explore/Logs.tsx
  47. 4 7
      public/app/features/explore/QueryEditor.tsx
  48. 68 50
      public/app/features/explore/QueryField.tsx
  49. 24 29
      public/app/features/explore/QueryRow.tsx
  50. 5 7
      public/app/features/explore/QueryRows.tsx
  51. 7 7
      public/app/features/explore/Wrapper.tsx
  52. 3 3
      public/app/features/explore/__snapshots__/Graph.test.tsx.snap
  53. 344 238
      public/app/features/explore/state/actionTypes.ts
  54. 162 252
      public/app/features/explore/state/actions.ts
  55. 35 30
      public/app/features/explore/state/reducers.test.ts
  56. 212 179
      public/app/features/explore/state/reducers.ts
  57. 3 0
      public/app/features/templating/template_srv.ts
  58. 17 0
      public/app/plugins/datasource/cloudwatch/datasource.ts
  59. 23 0
      public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
  60. 10 4
      public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx
  61. 20 25
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  62. 2 5
      public/app/plugins/datasource/loki/components/LokiStartPage.tsx
  63. 22 26
      public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
  64. 2 5
      public/app/plugins/datasource/prometheus/components/PromStart.tsx
  65. 4 4
      public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx
  66. 12 7
      public/app/plugins/datasource/stackdriver/datasource.ts
  67. binární
      public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png
  68. 0 0
      public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg
  69. 2 2
      public/app/plugins/datasource/stackdriver/plugin.json
  70. 2 2
      public/app/plugins/datasource/stackdriver/query_ctrl.ts
  71. 51 10
      public/app/plugins/datasource/stackdriver/specs/datasource.test.ts
  72. 12 8
      public/app/plugins/datasource/stackdriver/types.ts
  73. 22 0
      public/app/routes/GrafanaCtrl.ts
  74. 2 2
      public/app/store/configureStore.ts
  75. 26 11
      public/app/types/explore.ts
  76. 1 1
      public/img/icons_dark_theme/icon_advanced.svg
  77. 1 1
      public/img/icons_dark_theme/icon_advanced_active.svg
  78. 1 1
      public/img/icons_dark_theme/icon_alerting.svg
  79. 1 1
      public/img/icons_dark_theme/icon_alerting_active.svg
  80. 1 1
      public/img/icons_dark_theme/icon_query.svg
  81. 1 1
      public/img/icons_dark_theme/icon_query_active.svg
  82. 1 1
      public/img/icons_dark_theme/icon_visualize.svg
  83. 1 1
      public/img/icons_dark_theme/icon_visualize_active.svg
  84. 9 31
      public/sass/_variables.dark.scss
  85. 41 74
      public/sass/_variables.light.scss
  86. 1 1
      public/sass/base/_icons.scss
  87. 1 1
      public/sass/components/_code_editor.scss
  88. 4 0
      public/sass/components/_gf-form.scss
  89. 0 1
      public/sass/components/_toolbar.scss
  90. 9 1
      public/sass/utils/_utils.scss
  91. 12 13
      vendor/github.com/aws/aws-sdk-go/aws/awsutil/string_value.go
  92. 1 1
      vendor/github.com/aws/aws-sdk-go/aws/client/client.go
  93. 52 8
      vendor/github.com/aws/aws-sdk-go/aws/config.go
  94. 2 2
      vendor/github.com/aws/aws-sdk-go/aws/corehandlers/handlers.go
  95. 1 1
      vendor/github.com/aws/aws-sdk-go/aws/corehandlers/user_agent.go
  96. 1 3
      vendor/github.com/aws/aws-sdk-go/aws/credentials/chain_provider.go
  97. 40 6
      vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go
  98. 3 3
      vendor/github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds/ec2_role_provider.go
  99. 7 0
      vendor/github.com/aws/aws-sdk-go/aws/credentials/endpointcreds/provider.go
  100. 0 4
      vendor/github.com/aws/aws-sdk-go/aws/credentials/env_provider.go

+ 5 - 0
CHANGELOG.md

@@ -2,6 +2,11 @@
 
 ### Minor
 * **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
+* **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182)
+* **Cloudwatch**: Add `resource_arns` template variable query function [#8207](https://github.com/grafana/grafana/issues/8207), thx [@jeroenvollenbrock](https://github.com/jeroenvollenbrock)
+* **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson)
+* **Cloudwatch**: Add AWS RDS ServerlessDatabaseCapacity metric [#15265](https://github.com/grafana/grafana/pull/15265), thx [@larsjoergensen](https://github.com/larsjoergensen)
+* **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh)
 
 # 6.0.0-beta1 (2019-01-30)
 

+ 10 - 2
Gopkg.lock

@@ -37,6 +37,7 @@
     "aws/credentials",
     "aws/credentials/ec2rolecreds",
     "aws/credentials/endpointcreds",
+    "aws/credentials/processcreds",
     "aws/credentials/stscreds",
     "aws/csm",
     "aws/defaults",
@@ -45,13 +46,18 @@
     "aws/request",
     "aws/session",
     "aws/signer/v4",
+    "internal/ini",
+    "internal/s3err",
     "internal/sdkio",
     "internal/sdkrand",
+    "internal/sdkuri",
     "internal/shareddefaults",
     "private/protocol",
     "private/protocol/ec2query",
     "private/protocol/eventstream",
     "private/protocol/eventstream/eventstreamapi",
+    "private/protocol/json/jsonutil",
+    "private/protocol/jsonrpc",
     "private/protocol/query",
     "private/protocol/query/queryutil",
     "private/protocol/rest",
@@ -60,11 +66,13 @@
     "service/cloudwatch",
     "service/ec2",
     "service/ec2/ec2iface",
+    "service/resourcegroupstaggingapi",
+    "service/resourcegroupstaggingapi/resourcegroupstaggingapiiface",
     "service/s3",
     "service/sts"
   ]
-  revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
-  version = "v1.14.12"
+  revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2"
+  version = "v1.16.15"
 
 [[projects]]
   branch = "master"

+ 27 - 0
devenv/docker/blocks/loki/config.yaml

@@ -0,0 +1,27 @@
+server:
+  http_listen_port: 9080
+  grpc_listen_port: 0
+
+positions:
+  filename: /tmp/positions.yaml
+
+client:
+  url: http://loki:3100/api/prom/push
+
+scrape_configs:
+- job_name: system
+  entry_parser: raw
+  static_configs:
+  - targets:
+      - localhost
+    labels:
+      job: varlogs
+      __path__: /var/log/*log
+- job_name: grafana
+  entry_parser: raw
+  static_configs:
+  - targets:
+      - localhost
+    labels:
+      job: grafana
+      __path__: /var/log/grafana/*log

+ 2 - 10
devenv/docker/blocks/loki/docker-compose.yaml

@@ -1,22 +1,14 @@
-version: "3"
-
-networks:
-  loki:
-
-services:
   loki:
     image: grafana/loki:master
     ports:
       - "3100:3100"
     command: -config.file=/etc/loki/local-config.yaml
-    networks:
-      - loki
 
   promtail:
     image: grafana/promtail:master
     volumes:
+      - ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml
       - /var/log:/var/log
+      - ../data/log:/var/log/grafana
     command:
       -config.file=/etc/promtail/docker-config.yaml
-    networks:
-      - loki

+ 19 - 0
docs/sources/features/datasources/cloudwatch.md

@@ -74,6 +74,12 @@ Here is a minimal policy example:
                 "ec2:DescribeRegions"
             ],
             "Resource": "*"
+        },
+        {
+            "Sid": "AllowReadingResourcesForTags",
+            "Effect" : "Allow",      
+            "Action" : "tag:GetResources",      
+            "Resource" : "*"      
         }
     ]
 }
@@ -128,6 +134,7 @@ Name | Description
 *dimension_values(region, namespace, metric, dimension_key, [filters])* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric`, `dimension_key` or you can use dimension `filters` to get more specific result as well.
 *ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`.
 *ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attributes matching the specified `region`, `attribute_name`, `filters`.
+*resource_arns(region, resource_type, tags)* | Returns a list of ARNs matching the specified `region`, `resource_type` and `tags`.
 
 For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
 
@@ -143,6 +150,8 @@ Query | Service
 *dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
 *dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3
 *dimension_values(us-east-1,CWAgent,disk_used_percent,device,{"InstanceId":"$instance_id"})* | CloudWatch Agent
+*resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | ELB
+*resource_arns(eu-west-1,ec2:instance,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | EC2
 
 ## ec2_instance_attribute examples
 
@@ -205,6 +214,16 @@ Example `ec2_instance_attribute()` query
 ec2_instance_attribute(us-east-1, Tags.Name, { "tag:Team": [ "sysops" ] })
 ```
 
+## Using json format template variables
+
+Some of query takes JSON format filter. Grafana support to interpolate template variable to JSON format string, it can use as filter string.
+
+If `env = 'production', 'staging'`, following query will return ARNs of EC2 instances which `Environment` tag is `production` or `staging`.
+
+```
+resource_arns(us-east-1, ec2:instance, {"Environment":${env:json}})
+```
+
 ## Cost
 
 Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this,

+ 58 - 7
docs/sources/http_api/annotations.md

@@ -97,7 +97,7 @@ Creates an annotation in the Grafana database. The `dashboardId` and `panelId` f
 
 **Example Request**:
 
-```json
+```http
 POST /api/annotations HTTP/1.1
 Accept: application/json
 Content-Type: application/json
@@ -115,7 +115,7 @@ Content-Type: application/json
 
 **Example Response**:
 
-```json
+```http
 HTTP/1.1 200
 Content-Type: application/json
 
@@ -135,7 +135,7 @@ format (string with multiple tags being separated by a space).
 
 **Example Request**:
 
-```json
+```http
 POST /api/annotations/graphite HTTP/1.1
 Accept: application/json
 Content-Type: application/json
@@ -150,7 +150,7 @@ Content-Type: application/json
 
 **Example Response**:
 
-```json
+```http
 HTTP/1.1 200
 Content-Type: application/json
 
@@ -164,11 +164,14 @@ Content-Type: application/json
 
 `PUT /api/annotations/:id`
 
+Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the [Patch Annotation](#patch-annotation) operation.
+
 **Example Request**:
 
-```json
+```http
 PUT /api/annotations/1141 HTTP/1.1
 Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 Content-Type: application/json
 
 {
@@ -180,6 +183,50 @@ Content-Type: application/json
 }
 ```
 
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+    "message":"Annotation updated"
+}
+```
+
+## Patch Annotation
+
+`PATCH /api/annotations/:id`
+
+Updates one or more properties of an annotation that matches the specified id.
+
+This operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties. It does not handle updating of the `isRegion` and `regionId` properties. To make an annotation regional or vice versa, consider using the [Update Annotation](#update-annotation) operation.
+
+**Example Request**:
+
+```http
+PATCH /api/annotations/1145 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+Content-Type: application/json
+
+{
+  "text":"New Annotation Description",
+  "tags":["tag6","tag7","tag8"]
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+    "message":"Annotation patched"
+}
+```
+
 ## Delete Annotation By Id
 
 `DELETE /api/annotations/:id`
@@ -201,7 +248,9 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 HTTP/1.1 200
 Content-Type: application/json
 
-{"message":"Annotation deleted"}
+{
+    "message":"Annotation deleted"
+}
 ```
 
 ## Delete Annotation By RegionId
@@ -225,5 +274,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 HTTP/1.1 200
 Content-Type: application/json
 
-{"message":"Annotation region deleted"}
+{
+    "message":"Annotation region deleted"
+}
 ```

+ 1 - 3
docs/sources/installation/configuration.md

@@ -393,9 +393,7 @@ Analytics ID here. By default this feature is disabled.
 
 ### check_for_updates
 
-Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
-in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
-send any sensitive information.
+Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
 
 <hr />
 

+ 1 - 0
docs/sources/reference/templating.md

@@ -50,6 +50,7 @@ Filter Option | Example | Raw | Interpolated | Description
 `regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.&#124;test2)</code> | Formats multi-value variable into a regex string
 `pipe` | ${servers:pipe} | `'test.', 'test2'` |  <code>test.&#124;test2</code> | Formats multi-value variable into a pipe-separated string
 `csv`| ${servers:csv} |  `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
+`json`| ${servers:json} |  `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string
 `distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
 `lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
 `percentencode` | ${servers:percentencode} |  `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.

+ 1 - 1
packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx

@@ -49,7 +49,7 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
     return (
       <div className="gf-form-select-box__option-group">
         <div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
-          <span className="flex-grow">{label}</span>
+          <span className="flex-grow-1">{label}</span>
           <i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
         </div>
         {expanded && children}

+ 10 - 0
packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx

@@ -0,0 +1,10 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { ValueMappingsEditor } from './ValueMappingsEditor';
+
+const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module);
+
+ValueMappingsEditorStories.add('default', () => {
+  return <ValueMappingsEditor valueMappings={[]} onChange={action('Mapping changed')} />;
+});

+ 25 - 6
packages/grafana-ui/src/types/plugin.ts

@@ -1,6 +1,6 @@
 import { ComponentClass } from 'react';
 import { PanelProps, PanelOptionsProps } from './panel';
-import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
+import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
 
 export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
   /**
@@ -41,6 +41,12 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
   pluginExports?: PluginExports;
 }
 
+export interface ExploreDataSourceApi<TQuery extends DataQuery = DataQuery> extends DataSourceApi {
+  modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
+  getHighlighterExpression?(query: TQuery): string;
+  languageProvider?: any;
+}
+
 export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
   datasource: DSType;
   query: TQuery;
@@ -48,15 +54,30 @@ export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends D
   onChange: (value: TQuery) => void;
 }
 
+export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
+  datasource: DSType;
+  query: TQuery;
+  error?: string | JSX.Element;
+  hint?: QueryHint;
+  history: any[];
+  onExecuteQuery?: () => void;
+  onQueryChange?: (value: TQuery) => void;
+  onExecuteHint?: (action: QueryFixAction) => void;
+}
+
+export interface ExploreStartPageProps {
+  onClickExample: (query: DataQuery) => void;
+}
+
 export interface PluginExports {
   Datasource?: DataSourceApi;
   QueryCtrl?: any;
-  QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
+  QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, DataQuery>>;
   ConfigCtrl?: any;
   AnnotationsQueryCtrl?: any;
   VariableQueryEditor?: any;
-  ExploreQueryField?: any;
-  ExploreStartPage?: any;
+  ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, DataQuery>>;
+  ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
 
   // Panel plugin
   PanelCtrl?: any;
@@ -114,5 +135,3 @@ export interface PluginMetaInfo {
   updated: string;
   version: string;
 }
-
-

+ 59 - 0
pkg/api/annotations.go

@@ -210,6 +210,65 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
 	return Success("Annotation updated")
 }
 
+func PatchAnnotation(c *m.ReqContext, cmd dtos.PatchAnnotationsCmd) Response {
+	annotationID := c.ParamsInt64(":annotationId")
+
+	repo := annotations.GetRepository()
+
+	if resp := canSave(c, repo, annotationID); resp != nil {
+		return resp
+	}
+
+	items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
+
+	if err != nil || len(items) == 0 {
+		return Error(404, "Could not find annotation to update", err)
+	}
+
+	existing := annotations.Item{
+		OrgId:    c.OrgId,
+		UserId:   c.UserId,
+		Id:       annotationID,
+		Epoch:    items[0].Time,
+		Text:     items[0].Text,
+		Tags:     items[0].Tags,
+		RegionId: items[0].RegionId,
+	}
+
+	if cmd.Tags != nil {
+		existing.Tags = cmd.Tags
+	}
+
+	if cmd.Text != "" && cmd.Text != existing.Text {
+		existing.Text = cmd.Text
+	}
+
+	if cmd.Time > 0 && cmd.Time != existing.Epoch {
+		existing.Epoch = cmd.Time
+	}
+
+	if err := repo.Update(&existing); err != nil {
+		return Error(500, "Failed to update annotation", err)
+	}
+
+	// Update region end time if provided
+	if existing.RegionId != 0 && cmd.TimeEnd > 0 {
+		itemRight := existing
+		itemRight.RegionId = existing.Id
+		itemRight.Epoch = cmd.TimeEnd
+
+		// We don't know id of region right event, so set it to 0 and find then using query like
+		// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
+		itemRight.Id = 0
+
+		if err := repo.Update(&itemRight); err != nil {
+			return Error(500, "Failed to update annotation for region end time", err)
+		}
+	}
+
+	return Success("Annotation patched")
+}
+
 func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
 	repo := annotations.GetRepository()
 

+ 62 - 0
pkg/api/annotations_test.go

@@ -27,6 +27,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 			IsRegion: false,
 		}
 
+		patchCmd := dtos.PatchAnnotationsCmd{
+			Time: 1000,
+			Text: "annotation text",
+			Tags: []string{"tag1", "tag2"},
+		}
+
 		Convey("When user is an Org Viewer", func() {
 			role := m.ROLE_VIEWER
 			Convey("Should not be allowed to save an annotation", func() {
@@ -40,6 +46,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 
+				patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
+					sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
+					So(sc.resp.Code, ShouldEqual, 403)
+				})
+
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@@ -67,6 +78,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
 
+				patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
+					sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
+					So(sc.resp.Code, ShouldEqual, 200)
+				})
+
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@@ -100,6 +116,13 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 			Id:       1,
 		}
 
+		patchCmd := dtos.PatchAnnotationsCmd{
+			Time: 8000,
+			Text: "annotation text 50",
+			Tags: []string{"foo", "bar"},
+			Id:   1,
+		}
+
 		deleteCmd := dtos.DeleteAnnotationsCmd{
 			DashboardId: 1,
 			PanelId:     1,
@@ -136,6 +159,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 
+				patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
+					sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
+					So(sc.resp.Code, ShouldEqual, 403)
+				})
+
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@@ -163,6 +191,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
 
+				patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
+					sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
+					So(sc.resp.Code, ShouldEqual, 200)
+				})
+
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@@ -189,6 +222,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 					sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
+
+				patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
+					sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
+					So(sc.resp.Code, ShouldEqual, 200)
+				})
+
 				deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) {
 					sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
 					So(sc.resp.Code, ShouldEqual, 200)
@@ -264,6 +303,29 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
 	})
 }
 
+func patchAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		sc := setupScenarioContext(url)
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = role
+
+			return PatchAnnotation(c, cmd)
+		})
+
+		fakeAnnoRepo = &fakeAnnotationsRepo{}
+		annotations.SetRepository(fakeAnnoRepo)
+
+		sc.m.Patch(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}
+
 func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) {
 	Convey(desc+" "+url, func() {
 		defer bus.ClearBusHandlers()

+ 3 - 2
pkg/api/api.go

@@ -108,8 +108,8 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
 	r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
 
-	// api renew session based on remember cookie
-	r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
+	// api renew session based on cookie
+	r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing))
 
 	// authed api
 	r.Group("/api", func(apiRoute routing.RouteRegister) {
@@ -354,6 +354,7 @@ func (hs *HTTPServer) registerRoutes() {
 			annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation))
 			annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID))
 			annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation))
+			annotationsRoute.Patch("/:annotationId", bind(dtos.PatchAnnotationsCmd{}), Wrap(PatchAnnotation))
 			annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion))
 			annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation))
 		})

+ 1 - 1
pkg/api/common_test.go

@@ -149,4 +149,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC
 	return nil
 }
 
-func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
+func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil }

+ 8 - 0
pkg/api/dtos/annotations.go

@@ -22,6 +22,14 @@ type UpdateAnnotationsCmd struct {
 	TimeEnd  int64    `json:"timeEnd"`
 }
 
+type PatchAnnotationsCmd struct {
+	Id      int64    `json:"id"`
+	Time    int64    `json:"time"`
+	Text    string   `json:"text"`
+	Tags    []string `json:"tags"`
+	TimeEnd int64    `json:"timeEnd"`
+}
+
 type DeleteAnnotationsCmd struct {
 	AlertId      int64 `json:"alertId"`
 	DashboardId  int64 `json:"dashboardId"`

+ 1 - 1
pkg/api/login.go

@@ -136,7 +136,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
 }
 
 func (hs *HTTPServer) Logout(c *m.ReqContext) {
-	hs.AuthTokenService.UserSignedOutHook(c)
+	hs.AuthTokenService.SignOutUser(c)
 
 	if setting.SignoutRedirectUrl != "" {
 		c.Redirect(setting.SignoutRedirectUrl)

+ 1 - 1
pkg/middleware/middleware_test.go

@@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC
 	return nil
 }
 
-func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
+func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil }

+ 15 - 3
pkg/services/auth/auth_token.go

@@ -3,6 +3,7 @@ package auth
 import (
 	"crypto/sha256"
 	"encoding/hex"
+	"errors"
 	"net/http"
 	"net/url"
 	"time"
@@ -31,7 +32,7 @@ var (
 type UserAuthTokenService interface {
 	InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
 	UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
-	UserSignedOutHook(c *models.ReqContext)
+	SignOutUser(c *models.ReqContext) error
 }
 
 type UserAuthTokenServiceImpl struct {
@@ -85,7 +86,7 @@ func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext,
 
 func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
 	if setting.Env == setting.DEV {
-		ctx.Logger.Info("new token", "unhashed token", value)
+		ctx.Logger.Debug("new token", "unhashed token", value)
 	}
 
 	ctx.Resp.Header().Del("Set-Cookie")
@@ -112,8 +113,19 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m
 	return nil
 }
 
-func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) {
+func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error {
+	unhashedToken := c.GetCookie(s.Cfg.LoginCookieName)
+	if unhashedToken == "" {
+		return errors.New("cannot logout without session token")
+	}
+
+	hashedToken := hashToken(unhashedToken)
+
+	sql := `DELETE FROM user_auth_token WHERE auth_token = ?`
+	_, err := s.SQLStore.NewSession().Exec(sql, hashedToken)
+
 	s.writeSessionCookie(c, "", -1)
+	return err
 }
 
 func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {

+ 39 - 0
pkg/services/auth/auth_token_test.go

@@ -1,10 +1,15 @@
 package auth
 
 import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 	"time"
 
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
+	macaron "gopkg.in/macaron.v1"
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -46,6 +51,40 @@ func TestUserAuthToken(t *testing.T) {
 				So(err, ShouldEqual, ErrAuthTokenNotFound)
 				So(LookupToken, ShouldBeNil)
 			})
+
+			Convey("signing out should delete token and cookie if present", func() {
+				httpreq := &http.Request{Header: make(http.Header)}
+				httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken})
+
+				ctx := &models.ReqContext{Context: &macaron.Context{
+					Req:  macaron.Request{Request: httpreq},
+					Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
+				},
+					Logger: log.New("fakelogger"),
+				}
+
+				err = userAuthTokenService.SignOutUser(ctx)
+				So(err, ShouldBeNil)
+
+				// makes sure we tell the browser to overwrite the cookie
+				cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName)
+				So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader)
+			})
+
+			Convey("signing out an none existing session should return an error", func() {
+				httpreq := &http.Request{Header: make(http.Header)}
+				httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""})
+
+				ctx := &models.ReqContext{Context: &macaron.Context{
+					Req:  macaron.Request{Request: httpreq},
+					Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
+				},
+					Logger: log.New("fakelogger"),
+				}
+
+				err = userAuthTokenService.SignOutUser(ctx)
+				So(err, ShouldNotBeNil)
+			})
 		})
 
 		Convey("expires correctly", func() {

+ 1 - 4
pkg/services/sqlstore/sqlstore.go

@@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
 
 		cnnstr += ss.buildExtraConnectionString('&')
 	case migrator.POSTGRES:
-		host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432")
-		if err != nil {
-			return "", err
-		}
+		host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432")
 		if ss.dbCfg.Pwd == "" {
 			ss.dbCfg.Pwd = "''"
 		}

+ 3 - 1
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -21,6 +21,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
 	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
@@ -28,7 +29,8 @@ import (
 
 type CloudWatchExecutor struct {
 	*models.DataSource
-	ec2Svc ec2iface.EC2API
+	ec2Svc  ec2iface.EC2API
+	rgtaSvc resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
 }
 
 type DatasourceInfo struct {

+ 87 - 1
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -15,6 +15,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -95,10 +96,11 @@ func init() {
 		"AWS/Logs":             {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
 		"AWS/ML":               {"PredictCount", "PredictFailureCount"},
 		"AWS/NATGateway":       {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
+		"AWS/Neptune":          {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"},
 		"AWS/NetworkELB":       {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
 		"AWS/OpsWorks":         {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
 		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
-		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
 		"AWS/Route53":          {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
 		"AWS/S3":               {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
 		"AWS/SES":              {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
@@ -149,6 +151,7 @@ func init() {
 		"AWS/Logs":             {"LogGroupName", "DestinationType", "FilterName"},
 		"AWS/ML":               {"MLModelId", "RequestMode"},
 		"AWS/NATGateway":       {"NatGatewayId"},
+		"AWS/Neptune":          {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"},
 		"AWS/NetworkELB":       {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
 		"AWS/OpsWorks":         {"StackId", "LayerId", "InstanceId"},
 		"AWS/Redshift":         {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
@@ -198,6 +201,8 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
 		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
 	case "ec2_instance_attribute":
 		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
+	case "resource_arns":
+		data, err = e.handleGetResourceArns(ctx, parameters, queryContext)
 	}
 
 	transformToTable(data, queryResult)
@@ -534,6 +539,65 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
 	return result, nil
 }
 
+func (e *CloudWatchExecutor) ensureRGTAClientSession(region string) error {
+	if e.rgtaSvc == nil {
+		dsInfo := e.getDsInfo(region)
+		cfg, err := e.getAwsConfig(dsInfo)
+		if err != nil {
+			return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
+		}
+		sess, err := session.NewSession(cfg)
+		if err != nil {
+			return fmt.Errorf("Failed to call ec2:NewSession, %v", err)
+		}
+		e.rgtaSvc = resourcegroupstaggingapi.New(sess, cfg)
+	}
+	return nil
+}
+
+func (e *CloudWatchExecutor) handleGetResourceArns(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	resourceType := parameters.Get("resourceType").MustString()
+	filterJson := parameters.Get("tags").MustMap()
+
+	err := e.ensureRGTAClientSession(region)
+	if err != nil {
+		return nil, err
+	}
+
+	var filters []*resourcegroupstaggingapi.TagFilter
+	for k, v := range filterJson {
+		if vv, ok := v.([]interface{}); ok {
+			var vvvvv []*string
+			for _, vvv := range vv {
+				if vvvv, ok := vvv.(string); ok {
+					vvvvv = append(vvvvv, &vvvv)
+				}
+			}
+			filters = append(filters, &resourcegroupstaggingapi.TagFilter{
+				Key:    aws.String(k),
+				Values: vvvvv,
+			})
+		}
+	}
+
+	var resourceTypes []*string
+	resourceTypes = append(resourceTypes, &resourceType)
+
+	resources, err := e.resourceGroupsGetResources(region, filters, resourceTypes)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make([]suggestData, 0)
+	for _, resource := range resources.ResourceTagMappingList {
+		data := *resource.ResourceARN
+		result = append(result, suggestData{Text: data, Value: data})
+	}
+
+	return result, nil
+}
+
 func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
 	svc, err := e.getClient(region)
 	if err != nil {
@@ -585,6 +649,28 @@ func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.
 	return &resp, nil
 }
 
+func (e *CloudWatchExecutor) resourceGroupsGetResources(region string, filters []*resourcegroupstaggingapi.TagFilter, resourceTypes []*string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
+	params := &resourcegroupstaggingapi.GetResourcesInput{
+		ResourceTypeFilters: resourceTypes,
+		TagFilters:          filters,
+	}
+
+	var resp resourcegroupstaggingapi.GetResourcesOutput
+	err := e.rgtaSvc.GetResourcesPages(params,
+		func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
+			resources, _ := awsutil.ValuesAtPath(page, "ResourceTagMappingList")
+			for _, resource := range resources {
+				resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, resource.(*resourcegroupstaggingapi.ResourceTagMapping))
+			}
+			return !lastPage
+		})
+	if err != nil {
+		return nil, errors.New("Failed to call tags:GetResources")
+	}
+
+	return &resp, nil
+}
+
 func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
 	creds, err := GetCredentials(cwData)
 	if err != nil {

+ 57 - 0
pkg/tsdb/cloudwatch/metric_find_query_test.go

@@ -8,6 +8,8 @@ import (
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
+	"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
 	"github.com/bmizerany/assert"
 	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -22,6 +24,11 @@ type mockedEc2 struct {
 	RespRegions ec2.DescribeRegionsOutput
 }
 
+type mockedRGTA struct {
+	resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
+	Resp resourcegroupstaggingapi.GetResourcesOutput
+}
+
 func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
 	fn(&m.Resp, true)
 	return nil
@@ -30,6 +37,11 @@ func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeR
 	return &m.RespRegions, nil
 }
 
+func (m mockedRGTA) GetResourcesPages(in *resourcegroupstaggingapi.GetResourcesInput, fn func(*resourcegroupstaggingapi.GetResourcesOutput, bool) bool) error {
+	fn(&m.Resp, true)
+	return nil
+}
+
 func TestCloudWatchMetrics(t *testing.T) {
 
 	Convey("When calling getMetricsForCustomMetrics", t, func() {
@@ -209,6 +221,51 @@ func TestCloudWatchMetrics(t *testing.T) {
 			So(result[7].Text, ShouldEqual, "vol-4-2")
 		})
 	})
+
+	Convey("When calling handleGetResourceArns", t, func() {
+		executor := &CloudWatchExecutor{
+			rgtaSvc: mockedRGTA{
+				Resp: resourcegroupstaggingapi.GetResourcesOutput{
+					ResourceTagMappingList: []*resourcegroupstaggingapi.ResourceTagMapping{
+						{
+							ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567"),
+							Tags: []*resourcegroupstaggingapi.Tag{
+								{
+									Key:   aws.String("Environment"),
+									Value: aws.String("production"),
+								},
+							},
+						},
+						{
+							ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321"),
+							Tags: []*resourcegroupstaggingapi.Tag{
+								{
+									Key:   aws.String("Environment"),
+									Value: aws.String("production"),
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		json := simplejson.New()
+		json.Set("region", "us-east-1")
+		json.Set("resourceType", "ec2:instance")
+		tags := make(map[string]interface{})
+		tags["Environment"] = []string{"production"}
+		json.Set("tags", tags)
+		result, _ := executor.handleGetResourceArns(context.Background(), json, &tsdb.TsdbQuery{})
+
+		Convey("Should return all two instances", func() {
+			So(result[0].Text, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567")
+			So(result[0].Value, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567")
+			So(result[1].Text, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321")
+			So(result[1].Value, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321")
+
+		})
+	})
 }
 
 func TestParseMultiSelectValue(t *testing.T) {

+ 1 - 4
pkg/tsdb/mssql/mssql.go

@@ -49,10 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
 		}
 	}
 
-	server, port, err := util.SplitIPPort(datasource.Url, "1433")
-	if err != nil {
-		return "", err
-	}
+	server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
 
 	encrypt := datasource.JsonData.Get("encrypt").MustString("false")
 	connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",

+ 0 - 25
pkg/util/ip.go

@@ -1,25 +0,0 @@
-package util
-
-import (
-	"net"
-)
-
-// SplitIPPort splits the ip string and port.
-func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) {
-	ipAddr := net.ParseIP(ipStr)
-
-	if ipAddr == nil {
-		// Port was included
-		ip, port, err = net.SplitHostPort(ipStr)
-
-		if err != nil {
-			return "", "", err
-		}
-	} else {
-		// No port was included
-		ip = ipAddr.String()
-		port = portDefault
-	}
-
-	return ip, port, nil
-}

+ 31 - 6
pkg/util/ip_address.go

@@ -7,23 +7,48 @@ import (
 
 // ParseIPAddress parses an IP address and removes port and/or IPV6 format
 func ParseIPAddress(input string) string {
+	host, _ := SplitHostPort(input)
+
+	ip := net.ParseIP(host)
+
+	if ip == nil {
+		return host
+	}
+
+	if ip.IsLoopback() {
+		return "127.0.0.1"
+	}
+
+	return ip.String()
+}
+
+// SplitHostPortDefault splits ip address/hostname string by host and port. Defaults used if no match found
+func SplitHostPortDefault(input, defaultHost, defaultPort string) (host string, port string) {
+	port = defaultPort
 	s := input
 	lastIndex := strings.LastIndex(input, ":")
 
 	if lastIndex != -1 {
 		if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
 			s = input[:lastIndex]
+			port = input[lastIndex+1:]
+		} else if lastIndex == 0 {
+			s = defaultHost
+			port = input[lastIndex+1:]
 		}
+	} else {
+		port = defaultPort
 	}
 
 	s = strings.Replace(s, "[", "", -1)
 	s = strings.Replace(s, "]", "", -1)
+	port = strings.Replace(port, "[", "", -1)
+	port = strings.Replace(port, "]", "", -1)
 
-	ip := net.ParseIP(s)
-
-	if ip.IsLoopback() {
-		return "127.0.0.1"
-	}
+	return s, port
+}
 
-	return ip.String()
+// SplitHostPort splits ip address/hostname string by host and port
+func SplitHostPort(input string) (host string, port string) {
+	return SplitHostPortDefault(input, "", "")
 }

+ 83 - 1
pkg/util/ip_address_test.go

@@ -9,8 +9,90 @@ import (
 func TestParseIPAddress(t *testing.T) {
 	Convey("Test parse ip address", t, func() {
 		So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
+		So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
 		So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
 		So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
-		So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
+		So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1")
+		So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1")
+	})
+}
+
+func TestSplitHostPortDefault(t *testing.T) {
+	Convey("Test split ip address to host and port", t, func() {
+		host, port := SplitHostPortDefault("192.168.0.140:456", "", "")
+		So(host, ShouldEqual, "192.168.0.140")
+		So(port, ShouldEqual, "456")
+
+		host, port = SplitHostPortDefault("192.168.0.140", "", "123")
+		So(host, ShouldEqual, "192.168.0.140")
+		So(port, ShouldEqual, "123")
+
+		host, port = SplitHostPortDefault("[::1:456]", "", "")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "456")
+
+		host, port = SplitHostPortDefault("[::1]", "", "123")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "123")
+
+		host, port = SplitHostPortDefault("::1:123", "", "")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "123")
+
+		host, port = SplitHostPortDefault("::1", "", "123")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "123")
+
+		host, port = SplitHostPortDefault(":456", "1.2.3.4", "")
+		So(host, ShouldEqual, "1.2.3.4")
+		So(port, ShouldEqual, "456")
+
+		host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123")
+		So(host, ShouldEqual, "xyz.rds.amazonaws.com")
+		So(port, ShouldEqual, "123")
+
+		host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "")
+		So(host, ShouldEqual, "xyz.rds.amazonaws.com")
+		So(port, ShouldEqual, "123")
+	})
+}
+
+func TestSplitHostPort(t *testing.T) {
+	Convey("Test split ip address to host and port", t, func() {
+		host, port := SplitHostPort("192.168.0.140:456")
+		So(host, ShouldEqual, "192.168.0.140")
+		So(port, ShouldEqual, "456")
+
+		host, port = SplitHostPort("192.168.0.140")
+		So(host, ShouldEqual, "192.168.0.140")
+		So(port, ShouldEqual, "")
+
+		host, port = SplitHostPort("[::1:456]")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "456")
+
+		host, port = SplitHostPort("[::1]")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "")
+
+		host, port = SplitHostPort("::1:123")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "123")
+
+		host, port = SplitHostPort("::1")
+		So(host, ShouldEqual, "::1")
+		So(port, ShouldEqual, "")
+
+		host, port = SplitHostPort(":456")
+		So(host, ShouldEqual, "")
+		So(port, ShouldEqual, "456")
+
+		host, port = SplitHostPort("xyz.rds.amazonaws.com")
+		So(host, ShouldEqual, "xyz.rds.amazonaws.com")
+		So(port, ShouldEqual, "")
+
+		host, port = SplitHostPort("xyz.rds.amazonaws.com:123")
+		So(host, ShouldEqual, "xyz.rds.amazonaws.com")
+		So(port, ShouldEqual, "123")
 	})
 }

+ 0 - 43
pkg/util/ip_test.go

@@ -1,43 +0,0 @@
-package util
-
-import (
-	"testing"
-
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-func TestSplitIPPort(t *testing.T) {
-
-	Convey("When parsing an IPv4 without explicit port", t, func() {
-		ip, port, err := SplitIPPort("1.2.3.4", "5678")
-
-		So(err, ShouldEqual, nil)
-		So(ip, ShouldEqual, "1.2.3.4")
-		So(port, ShouldEqual, "5678")
-	})
-
-	Convey("When parsing an IPv6 without explicit port", t, func() {
-		ip, port, err := SplitIPPort("::1", "5678")
-
-		So(err, ShouldEqual, nil)
-		So(ip, ShouldEqual, "::1")
-		So(port, ShouldEqual, "5678")
-	})
-
-	Convey("When parsing an IPv4 with explicit port", t, func() {
-		ip, port, err := SplitIPPort("1.2.3.4:56", "78")
-
-		So(err, ShouldEqual, nil)
-		So(ip, ShouldEqual, "1.2.3.4")
-		So(port, ShouldEqual, "56")
-	})
-
-	Convey("When parsing an IPv6 with explicit port", t, func() {
-		ip, port, err := SplitIPPort("[::1]:56", "78")
-
-		So(err, ShouldEqual, nil)
-		So(ip, ShouldEqual, "::1")
-		So(port, ShouldEqual, "56")
-	})
-
-}

+ 26 - 11
public/app/core/services/backend_srv.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
+import config from 'app/core/config';
 import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
 
 export class BackendSrv {
@@ -103,10 +104,17 @@ export class BackendSrv {
       err => {
         // handle unauthorized
         if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
-          return this.loginPing().then(() => {
-            options.retry = 1;
-            return this.request(options);
-          });
+          return this.loginPing()
+            .then(() => {
+              options.retry = 1;
+              return this.request(options);
+            })
+            .catch(err => {
+              if (err.status === 401) {
+                window.location.href = config.appSubUrl + '/logout';
+                throw err;
+              }
+            });
         }
 
         this.$timeout(this.requestErrorHandler.bind(this, err), 50);
@@ -184,13 +192,20 @@ export class BackendSrv {
 
         // handle unauthorized for backend requests
         if (requestIsLocal && firstAttempt && err.status === 401) {
-          return this.loginPing().then(() => {
-            options.retry = 1;
-            if (canceler) {
-              canceler.resolve();
-            }
-            return this.datasourceRequest(options);
-          });
+          return this.loginPing()
+            .then(() => {
+              options.retry = 1;
+              if (canceler) {
+                canceler.resolve();
+              }
+              return this.datasourceRequest(options);
+            })
+            .catch(err => {
+              if (err.status === 401) {
+                window.location.href = config.appSubUrl + '/logout';
+                throw err;
+              }
+            });
         }
 
         // populate error obj on Internal Error

+ 30 - 2
public/app/core/utils/explore.test.ts

@@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
   datasource: null,
   queries: [],
   range: DEFAULT_RANGE,
+  ui: {
+    showingGraph: true,
+    showingTable: true,
+    showingLogs: true,
+  }
 };
 
 describe('state functions', () => {
@@ -69,9 +74,11 @@ describe('state functions', () => {
           to: 'now',
         },
       };
+
       expect(serializeStateToUrlParam(state)).toBe(
         '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
-          '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
+          '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
+          '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}'
       );
     });
 
@@ -93,7 +100,7 @@ describe('state functions', () => {
         },
       };
       expect(serializeStateToUrlParam(state, true)).toBe(
-        '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
+        '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]'
       );
     });
   });
@@ -118,7 +125,28 @@ describe('state functions', () => {
       };
       const serialized = serializeStateToUrlParam(state);
       const parsed = parseUrlState(serialized);
+      expect(state).toMatchObject(parsed);
+    });
 
+    it('can parse the compact serialized state into the original state', () => {
+      const state = {
+        ...DEFAULT_EXPLORE_STATE,
+        datasource: 'foo',
+        queries: [
+          {
+            expr: 'metric{test="a/b"}',
+          },
+          {
+            expr: 'super{foo="x/z"}',
+          },
+        ],
+        range: {
+          from: 'now - 5h',
+          to: 'now',
+        },
+      };
+      const serialized = serializeStateToUrlParam(state, true);
+      const parsed = parseUrlState(serialized);
       expect(state).toMatchObject(parsed);
     });
   });

+ 46 - 5
public/app/core/utils/explore.ts

@@ -11,7 +11,7 @@ import { colors } from '@grafana/ui';
 import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
 
 // Types
-import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types';
+import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui/src/types';
 import TimeSeries from 'app/core/time_series2';
 import {
   ExploreUrlState,
@@ -27,6 +27,12 @@ export const DEFAULT_RANGE = {
   to: 'now',
 };
 
+export const DEFAULT_UI_STATE = {
+  showingTable: true,
+  showingGraph: true,
+  showingLogs: true,
+};
+
 const MAX_HISTORY_ITEMS = 100;
 
 export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
@@ -147,7 +153,12 @@ export function buildQueryTransaction(
 
 export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
 
+const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
+const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
+
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
+  let uiState = DEFAULT_UI_STATE;
+
   if (initial) {
     try {
       const parsed = JSON.parse(decodeURI(initial));
@@ -160,20 +171,41 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
           to: parsed[1],
         };
         const datasource = parsed[2];
-        const queries = parsed.slice(3);
-        return { datasource, queries, range };
+        let queries = [];
+
+        parsed.slice(3).forEach(segment => {
+          if (isMetricSegment(segment)) {
+            queries = [...queries, segment];
+          }
+
+          if (isUISegment(segment)) {
+            uiState = {
+              showingGraph: segment.ui[0],
+              showingLogs: segment.ui[1],
+              showingTable: segment.ui[2],
+            };
+          }
+        });
+
+        return { datasource, queries, range, ui: uiState };
       }
       return parsed;
     } catch (e) {
       console.error(e);
     }
   }
-  return { datasource: null, queries: [], range: DEFAULT_RANGE };
+  return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: uiState };
 }
 
 export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
   if (compact) {
-    return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
+    return JSON.stringify([
+      urlState.range.from,
+      urlState.range.to,
+      urlState.datasource,
+      ...urlState.queries,
+      { ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] },
+    ]);
   }
   return JSON.stringify(urlState);
 }
@@ -304,3 +336,12 @@ export function clearHistory(datasourceId: string) {
   const historyKey = `grafana.explore.history.${datasourceId}`;
   store.delete(historyKey);
 }
+
+export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => {
+  const queryKeys = queries.reduce((newQueryKeys, query, index) => {
+    const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
+    return newQueryKeys.concat(`${primaryKey}-${index}`);
+  }, []);
+
+  return queryKeys;
+};

+ 23 - 0
public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { AddPanelWidget, Props } from './AddPanelWidget';
+import { DashboardModel, PanelModel } from '../../state';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    dashboard: {} as DashboardModel,
+    panel: {} as PanelModel,
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<AddPanelWidget {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 61 - 39
public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx

@@ -1,12 +1,20 @@
+// Libraries
 import React from 'react';
 import _ from 'lodash';
+
+// Utils
 import config from 'app/core/config';
-import { PanelModel } from '../../state/PanelModel';
-import { DashboardModel } from '../../state/DashboardModel';
 import store from 'app/core/store';
-import { LS_PANEL_COPY_KEY } from 'app/core/constants';
-import { updateLocation } from 'app/core/actions';
+
+// Store
 import { store as reduxStore } from 'app/store/store';
+import { updateLocation } from 'app/core/actions';
+
+// Types
+import { PanelModel } from '../../state';
+import { DashboardModel } from '../../state';
+import { LS_PANEL_COPY_KEY } from 'app/core/constants';
+import { LocationUpdate } from 'app/types';
 
 export interface Props {
   panel: PanelModel;
@@ -46,6 +54,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
         copiedPanels.push(pluginCopy);
       }
     }
+
     return _.sortBy(copiedPanels, 'sort');
   }
 
@@ -54,28 +63,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
     this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
   }
 
-  copyButton(panel) {
-    return (
-      <button className="btn-inverse btn" onClick={() => this.onPasteCopiedPanel(panel)} title={panel.name}>
-        Paste copied Panel
-      </button>
-    );
-  }
-
-  moveToEdit(panel) {
-    reduxStore.dispatch(
-      updateLocation({
-        query: {
-          panelId: panel.id,
-          edit: true,
-          fullscreen: true,
-        },
-        partial: true,
-      })
-    );
-  }
-
-  onCreateNewPanel = () => {
+  onCreateNewPanel = (tab = 'queries') => {
     const dashboard = this.props.dashboard;
     const { gridPos } = this.props.panel;
 
@@ -88,7 +76,21 @@ export class AddPanelWidget extends React.Component<Props, State> {
     dashboard.addPanel(newPanel);
     dashboard.removePanel(this.props.panel);
 
-    this.moveToEdit(newPanel);
+    const location: LocationUpdate = {
+      query: {
+        panelId: newPanel.id,
+        edit: true,
+        fullscreen: true,
+      },
+      partial: true,
+    };
+
+    if (tab === 'visualization') {
+      location.query.tab = 'visualization';
+      location.query.openVizPicker  = true;
+    }
+
+    reduxStore.dispatch(updateLocation(location));
   };
 
   onPasteCopiedPanel = panelPluginInfo => {
@@ -125,30 +127,50 @@ export class AddPanelWidget extends React.Component<Props, State> {
     dashboard.removePanel(this.props.panel);
   };
 
-  render() {
-    let addCopyButton;
+  renderOptionLink = (icon, text, onClick) => {
+    return (
+      <div>
+        <a href="#" onClick={onClick} className="add-panel-widget__link btn btn-inverse">
+          <div className="add-panel-widget__icon">
+            <i className={`gicon gicon-${icon}`} />
+          </div>
+          <span>{text}</span>
+        </a>
+      </div>
+    );
+  };
 
-    if (this.state.copiedPanelPlugins.length === 1) {
-      addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]);
-    }
+  render() {
+    const { copiedPanelPlugins } = this.state;
 
     return (
       <div className="panel-container add-panel-widget-container">
         <div className="add-panel-widget">
           <div className="add-panel-widget__header grid-drag-handle">
             <i className="gicon gicon-add-panel" />
+            <span className="add-panel-widget__title">New Panel</span>
             <button className="add-panel-widget__close" onClick={this.handleCloseAddPanel}>
               <i className="fa fa-close" />
             </button>
           </div>
           <div className="add-panel-widget__btn-container">
-            <button className="btn-success btn btn-large" onClick={this.onCreateNewPanel}>
-              Edit Panel
-            </button>
-            {addCopyButton}
-            <button className="btn-inverse btn" onClick={this.onCreateNewRow}>
-              Add Row
-            </button>
+            <div className="add-panel-widget__create">
+              {this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)}
+              {this.renderOptionLink('visualization', 'Choose Visualization', () =>
+                this.onCreateNewPanel('visualization')
+              )}
+            </div>
+            <div className="add-panel-widget__actions">
+              <button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>Convert to row</button>
+              {copiedPanelPlugins.length === 1 && (
+                <button
+                  className="btn btn-inverse add-panel-widget__action"
+                  onClick={() => this.onPasteCopiedPanel(copiedPanelPlugins[0])}
+                >
+                  Paste copied panel
+                </button>
+              )}
+            </div>
           </div>
         </div>
       </div>

+ 42 - 5
public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss

@@ -14,6 +14,9 @@
   align-items: center;
   width: 100%;
   cursor: move;
+  background: $page-header-bg;
+  box-shadow: $page-header-shadow;
+  border-bottom: 1px solid $page-header-border-color;
 
   .gicon {
     font-size: 30px;
@@ -26,6 +29,29 @@
   }
 }
 
+.add-panel-widget__title {
+  font-size: $font-size-md;
+  font-weight: $font-weight-semi-bold;
+  margin-right: $spacer*2;
+}
+
+.add-panel-widget__link {
+  margin: 0 8px;
+  width: 154px;
+}
+
+.add-panel-widget__icon {
+  margin-bottom: 8px;
+
+  .gicon {
+    color: white;
+    height: 44px;
+    width: 53px;
+    position: relative;
+    left: 5px;
+  }
+}
+
 .add-panel-widget__close {
   margin-left: auto;
   background-color: transparent;
@@ -34,14 +60,25 @@
   margin-right: -10px;
 }
 
+.add-panel-widget__create {
+  display: inherit;
+  margin-bottom: 24px;
+  // this is to have the big button appear centered
+  margin-top: 55px;
+}
+
+.add-panel-widget__actions {
+  display: inherit;
+}
+
+.add-panel-widget__action {
+  margin: 0 4px;
+}
+
 .add-panel-widget__btn-container {
+  height: 100%;
   display: flex;
   justify-content: center;
   align-items: center;
-  height: 100%;
   flex-direction: column;
-
-  .btn {
-    margin-bottom: 10px;
-  }
 }

+ 86 - 0
public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap

@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="panel-container add-panel-widget-container"
+>
+  <div
+    className="add-panel-widget"
+  >
+    <div
+      className="add-panel-widget__header grid-drag-handle"
+    >
+      <i
+        className="gicon gicon-add-panel"
+      />
+      <span
+        className="add-panel-widget__title"
+      >
+        New Panel
+      </span>
+      <button
+        className="add-panel-widget__close"
+        onClick={[Function]}
+      >
+        <i
+          className="fa fa-close"
+        />
+      </button>
+    </div>
+    <div
+      className="add-panel-widget__btn-container"
+    >
+      <div
+        className="add-panel-widget__create"
+      >
+        <div>
+          <a
+            className="add-panel-widget__link btn btn-inverse"
+            href="#"
+            onClick={[Function]}
+          >
+            <div
+              className="add-panel-widget__icon"
+            >
+              <i
+                className="gicon gicon-queries"
+              />
+            </div>
+            <span>
+              Add Query
+            </span>
+          </a>
+        </div>
+        <div>
+          <a
+            className="add-panel-widget__link btn btn-inverse"
+            href="#"
+            onClick={[Function]}
+          >
+            <div
+              className="add-panel-widget__icon"
+            >
+              <i
+                className="gicon gicon-visualization"
+              />
+            </div>
+            <span>
+              Choose Visualization
+            </span>
+          </a>
+        </div>
+      </div>
+      <div
+        className="add-panel-widget__actions"
+      >
+        <button
+          className="btn btn-inverse add-panel-widget__action"
+          onClick={[Function]}
+        >
+          Convert to row
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+`;

+ 1 - 0
public/app/features/dashboard/components/SaveModals/index.ts

@@ -1,2 +1,3 @@
 export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
 export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
+export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';

+ 2 - 2
public/app/features/dashboard/panel_editor/PanelEditor.tsx

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 
 import { QueriesTab } from './QueriesTab';
-import { VisualizationTab } from './VisualizationTab';
+import VisualizationTab from './VisualizationTab';
 import { GeneralTab } from './GeneralTab';
 import { AlertTab } from '../../alerting/AlertTab';
 
@@ -38,7 +38,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
   onChangeTab = (tab: PanelEditorTab) => {
     store.dispatch(
       updateLocation({
-        query: { tab: tab.id },
+        query: { tab: tab.id, openVizPicker: null },
         partial: true,
       })
     );

+ 1 - 1
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -133,7 +133,7 @@ export class QueriesTab extends PureComponent<Props, State> {
     return (
       <>
         <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
-        <div className="flex-grow" />
+        <div className="flex-grow-1" />
         {!isAddingMixed && (
           <button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
             Add Query

+ 20 - 1
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -3,6 +3,9 @@ import React, { PureComponent } from 'react';
 
 // Utils & Services
 import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
+import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
+import { StoreState } from 'app/types';
+import { updateLocation } from 'app/core/actions';
 
 // Components
 import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
@@ -21,6 +24,8 @@ interface Props {
   plugin: PanelPlugin;
   angularPanel?: AngularComponent;
   onTypeChanged: (newType: PanelPlugin) => void;
+  updateLocation: typeof updateLocation;
+  urlOpenVizPicker: boolean;
 }
 
 interface State {
@@ -38,7 +43,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
     super(props);
 
     this.state = {
-      isVizPickerOpen: false,
+      isVizPickerOpen: this.props.urlOpenVizPicker,
       searchQuery: '',
       scrollTop: 0,
     };
@@ -149,6 +154,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
   };
 
   onCloseVizPicker = () => {
+    if (this.props.urlOpenVizPicker) {
+      this.props.updateLocation({ query: { openVizPicker: null }, partial: true });
+    }
+
     this.setState({ isVizPickerOpen: false });
   };
 
@@ -236,3 +245,13 @@ export class VisualizationTab extends PureComponent<Props, State> {
     );
   }
 }
+
+const mapStateToProps = (state: StoreState) => ({
+  urlOpenVizPicker: !!state.location.query.openVizPicker
+});
+
+const mapDispatchToProps = {
+  updateLocation
+};
+
+export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps);

+ 20 - 25
public/app/features/explore/Explore.tsx

@@ -1,5 +1,5 @@
 // Libraries
-import React from 'react';
+import React, { ComponentClass } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import _ from 'lodash';
@@ -18,34 +18,26 @@ import TableContainer from './TableContainer';
 import TimePicker, { parseTime } from './TimePicker';
 
 // Actions
-import {
-  changeSize,
-  changeTime,
-  initializeExplore,
-  modifyQueries,
-  scanStart,
-  scanStop,
-  setQueries,
-} from './state/actions';
+import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
 
 // Types
-import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
+import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
 import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
 import { StoreState } from 'app/types';
-import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
+import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
 import { Emitter } from 'app/core/utils/emitter';
 import { ExploreToolbar } from './ExploreToolbar';
+import { scanStopAction } from './state/actionTypes';
 
 interface ExploreProps {
-  StartPage?: any;
+  StartPage?: ComponentClass<ExploreStartPageProps>;
   changeSize: typeof changeSize;
   changeTime: typeof changeTime;
   datasourceError: string;
-  datasourceInstance: any;
+  datasourceInstance: ExploreDataSourceApi;
   datasourceLoading: boolean | null;
   datasourceMissing: boolean;
   exploreId: ExploreId;
-  initialQueries: DataQuery[];
   initializeExplore: typeof initializeExplore;
   initialized: boolean;
   modifyQueries: typeof modifyQueries;
@@ -54,7 +46,7 @@ interface ExploreProps {
   scanning?: boolean;
   scanRange?: RawTimeRange;
   scanStart: typeof scanStart;
-  scanStop: typeof scanStop;
+  scanStopAction: typeof scanStopAction;
   setQueries: typeof setQueries;
   split: boolean;
   showingStartPage?: boolean;
@@ -62,6 +54,7 @@ interface ExploreProps {
   supportsLogs: boolean | null;
   supportsTable: boolean | null;
   urlState: ExploreUrlState;
+  queryKeys: string[];
 }
 
 /**
@@ -107,18 +100,20 @@ export class Explore extends React.PureComponent<ExploreProps> {
     // Don't initialize on split, but need to initialize urlparameters when present
     if (!initialized) {
       // Load URL state and parse range
-      const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState;
+      const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
       const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
       const initialQueries: DataQuery[] = ensureQueries(queries);
       const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
       const width = this.el ? this.el.offsetWidth : 0;
+
       this.props.initializeExplore(
         exploreId,
         initialDatasource,
         initialQueries,
         initialRange,
         width,
-        this.exploreEvents
+        this.exploreEvents,
+        ui
       );
     }
   }
@@ -171,7 +166,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
   };
 
   onStopScanning = () => {
-    this.props.scanStop(this.props.exploreId);
+    this.props.scanStopAction({ exploreId: this.props.exploreId });
   };
 
   render() {
@@ -182,12 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps> {
       datasourceLoading,
       datasourceMissing,
       exploreId,
-      initialQueries,
       showingStartPage,
       split,
       supportsGraph,
       supportsLogs,
       supportsTable,
+      queryKeys,
     } = this.props;
     const exploreClass = split ? 'explore explore-split' : 'explore';
 
@@ -208,7 +203,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
         {datasourceInstance &&
           !datasourceError && (
             <div className="explore-container">
-              <QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} initialQueries={initialQueries} />
+              <QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
               <AutoSizer onResize={this.onResize} disableHeight>
                 {({ width }) => (
                   <main className="m-t-2" style={{ width }}>
@@ -216,7 +211,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
                       {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
                       {!showingStartPage && (
                         <>
-                          {supportsGraph && !supportsLogs && <GraphContainer exploreId={exploreId} />}
+                          {supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
                           {supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
                           {supportsLogs && (
                             <LogsContainer
@@ -250,13 +245,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     datasourceInstance,
     datasourceLoading,
     datasourceMissing,
-    initialQueries,
     initialized,
     range,
     showingStartPage,
     supportsGraph,
     supportsLogs,
     supportsTable,
+    queryKeys,
   } = item;
   return {
     StartPage,
@@ -264,7 +259,6 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     datasourceInstance,
     datasourceLoading,
     datasourceMissing,
-    initialQueries,
     initialized,
     range,
     showingStartPage,
@@ -272,6 +266,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     supportsGraph,
     supportsLogs,
     supportsTable,
+    queryKeys,
   };
 }
 
@@ -281,7 +276,7 @@ const mapDispatchToProps = {
   initializeExplore,
   modifyQueries,
   scanStart,
-  scanStop,
+  scanStopAction,
   setQueries,
 };
 

+ 8 - 1
public/app/features/explore/ExploreToolbar.tsx

@@ -8,6 +8,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { StoreState } from 'app/types/store';
 import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions';
 import TimePicker from './TimePicker';
+import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
 
 enum IconSide {
   left = 'left',
@@ -79,6 +80,10 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
     this.props.runQuery(this.props.exploreId);
   };
 
+  onCloseTimePicker = () => {
+    this.props.timepickerRef.current.setState({ isOpen: false });
+  };
+
   render() {
     const {
       datasourceMissing,
@@ -137,7 +142,9 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
               </div>
             ) : null}
             <div className="explore-toolbar-content-item timepicker">
-              <TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
+              <ClickOutsideWrapper onClick={this.onCloseTimePicker}>
+                <TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
+              </ClickOutsideWrapper>
             </div>
             <div className="explore-toolbar-content-item">
               <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>

+ 1 - 0
public/app/features/explore/Graph.test.tsx

@@ -5,6 +5,7 @@ import { mockData } from './__mocks__/mockData';
 
 const setup = (propOverrides?: object) => {
   const props = {
+    size: { width: 10, height: 20 },
     data: mockData().slice(0, 19),
     range: { from: 'now-6h', to: 'now' },
     ...propOverrides,

+ 7 - 8
public/app/features/explore/Graph.tsx

@@ -1,7 +1,6 @@
 import $ from 'jquery';
 import React, { PureComponent } from 'react';
 import moment from 'moment';
-import { withSize } from 'react-sizeme';
 
 import 'vendor/flot/jquery.flot';
 import 'vendor/flot/jquery.flot.time';
@@ -76,11 +75,11 @@ const FLOT_OPTIONS = {
 
 interface GraphProps {
   data: any[];
-  height?: string; // e.g., '200px'
+  height?: number;
+  width?: number;
   id?: string;
   range: RawTimeRange;
   split?: boolean;
-  size?: { width: number; height: number };
   userOptions?: any;
   onChangeTime?: (range: RawTimeRange) => void;
   onToggleSeries?: (alias: string, hiddenSeries: Set<string>) => void;
@@ -122,7 +121,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
       prevProps.range !== this.props.range ||
       prevProps.split !== this.props.split ||
       prevProps.height !== this.props.height ||
-      (prevProps.size && prevProps.size.width !== this.props.size.width) ||
+      prevProps.width !== this.props.width ||
       !equal(prevState.hiddenSeries, this.state.hiddenSeries)
     ) {
       this.draw();
@@ -144,8 +143,8 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
   };
 
   getDynamicOptions() {
-    const { range, size } = this.props;
-    const ticks = (size.width || 0) / 100;
+    const { range, width } = this.props;
+    const ticks = (width || 0) / 100;
     let { from, to } = range;
     if (!moment.isMoment(from)) {
       from = dateMath.parse(from, false);
@@ -237,7 +236,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
   }
 
   render() {
-    const { height = '100px', id = 'graph' } = this.props;
+    const { height = 100, id = 'graph' } = this.props;
     const { hiddenSeries } = this.state;
     const data = this.getGraphData();
 
@@ -261,4 +260,4 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
   }
 }
 
-export default withSize()(Graph);
+export default Graph;

+ 4 - 2
public/app/features/explore/GraphContainer.tsx

@@ -20,6 +20,7 @@ interface GraphContainerProps {
   split: boolean;
   toggleGraph: typeof toggleGraph;
   changeTime: typeof changeTime;
+  width: number;
 }
 
 export class GraphContainer extends PureComponent<GraphContainerProps> {
@@ -32,8 +33,8 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
   };
 
   render() {
-    const { exploreId, graphResult, loading, showingGraph, showingTable, range, split } = this.props;
-    const graphHeight = showingGraph && showingTable ? '200px' : '400px';
+    const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width } = this.props;
+    const graphHeight = showingGraph && showingTable ? 200 : 400;
 
     if (!graphResult) {
       return null;
@@ -48,6 +49,7 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
           onChangeTime={this.onChangeTime}
           range={range}
           split={split}
+          width={width}
         />
       </Panel>
     );

+ 1 - 1
public/app/features/explore/Logs.tsx

@@ -214,7 +214,7 @@ export default class Logs extends PureComponent<Props, State> {
         <div className="logs-panel-graph">
           <Graph
             data={timeSeries}
-            height="100px"
+            height={100}
             range={range}
             id={`explore-logs-graph-${exploreId}`}
             onChangeTime={this.props.onChangeTime}

+ 4 - 7
public/app/features/explore/QueryEditor.tsx

@@ -14,7 +14,7 @@ interface QueryEditorProps {
   datasource: any;
   error?: string | JSX.Element;
   onExecuteQuery?: () => void;
-  onQueryChange?: (value: DataQuery, override?: boolean) => void;
+  onQueryChange?: (value: DataQuery) => void;
   initialQuery: DataQuery;
   exploreEvents: Emitter;
   range: RawTimeRange;
@@ -40,20 +40,17 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
         datasource,
         target,
         refresh: () => {
-          this.props.onQueryChange(target, false);
+          this.props.onQueryChange(target);
           this.props.onExecuteQuery();
         },
         events: exploreEvents,
-        panel: {
-          datasource,
-          targets: [target],
-        },
+        panel: { datasource, targets: [target] },
         dashboard: {},
       },
     };
 
     this.component = loader.load(this.element, scopeProps, template);
-    this.props.onQueryChange(target, false);
+    this.props.onQueryChange(target);
   }
 
   componentWillUnmount() {

+ 68 - 50
public/app/features/explore/QueryField.tsx

@@ -33,10 +33,9 @@ export interface QueryFieldProps {
   cleanText?: (text: string) => string;
   disabled?: boolean;
   initialQuery: string | null;
-  onBlur?: () => void;
-  onFocus?: () => void;
+  onExecuteQuery?: () => void;
+  onQueryChange?: (value: string) => void;
   onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
-  onValueChanged?: (value: string) => void;
   onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
   placeholder?: string;
   portalOrigin?: string;
@@ -51,6 +50,7 @@ export interface QueryFieldState {
   typeaheadPrefix: string;
   typeaheadText: string;
   value: Value;
+  lastExecutedValue: Value;
 }
 
 export interface TypeaheadInput {
@@ -90,6 +90,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
       typeaheadPrefix: '',
       typeaheadText: '',
       value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
+      lastExecutedValue: null,
     };
   }
 
@@ -132,11 +133,11 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
       if (this.placeholdersBuffer.hasPlaceholders()) {
         change.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
       }
-      this.onChange(change);
+      this.onChange(change, true);
     }
   }
 
-  onChange = ({ value }) => {
+  onChange = ({ value }, invokeParentOnValueChanged?: boolean) => {
     const documentChanged = value.document !== this.state.value.document;
     const prevValue = this.state.value;
 
@@ -144,8 +145,8 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     this.setState({ value }, () => {
       if (documentChanged) {
         const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value);
-        if (textChanged) {
-          this.handleChangeValue();
+        if (textChanged && invokeParentOnValueChanged) {
+          this.executeOnQueryChangeAndExecuteQueries();
         }
       }
     });
@@ -159,11 +160,16 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     }
   };
 
-  handleChangeValue = () => {
+  executeOnQueryChangeAndExecuteQueries = () => {
     // Send text change to parent
-    const { onValueChanged } = this.props;
-    if (onValueChanged) {
-      onValueChanged(Plain.serialize(this.state.value));
+    const { onQueryChange, onExecuteQuery } = this.props;
+    if (onQueryChange) {
+      onQueryChange(Plain.serialize(this.state.value));
+    }
+
+    if (onExecuteQuery) {
+      onExecuteQuery();
+      this.setState({ lastExecutedValue: this.state.value });
     }
   };
 
@@ -288,8 +294,37 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
       .focus();
   }
 
-  onKeyDown = (event, change) => {
+  handleEnterAndTabKey = change => {
     const { typeaheadIndex, suggestions } = this.state;
+    if (this.menuEl) {
+      // Dont blur input
+      event.preventDefault();
+      if (!suggestions || suggestions.length === 0) {
+        return undefined;
+      }
+
+      const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
+      const nextChange = this.applyTypeahead(change, suggestion);
+
+      const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text');
+      if (insertTextOperation) {
+        const suggestionText = insertTextOperation.text;
+        this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
+        if (this.placeholdersBuffer.hasPlaceholders()) {
+          nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
+        }
+      }
+
+      return true;
+    } else {
+      this.executeOnQueryChangeAndExecuteQueries();
+
+      return undefined;
+    }
+  };
+
+  onKeyDown = (event, change) => {
+    const { typeaheadIndex } = this.state;
 
     switch (event.key) {
       case 'Escape': {
@@ -312,27 +347,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
       }
       case 'Enter':
       case 'Tab': {
-        if (this.menuEl) {
-          // Dont blur input
-          event.preventDefault();
-          if (!suggestions || suggestions.length === 0) {
-            return undefined;
-          }
-
-          const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
-          const nextChange = this.applyTypeahead(change, suggestion);
-
-          const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text');
-          if (insertTextOperation) {
-            const suggestionText = insertTextOperation.text;
-            this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
-            if (this.placeholdersBuffer.hasPlaceholders()) {
-              nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
-            }
-          }
-
-          return true;
-        }
+        return this.handleEnterAndTabKey(change);
         break;
       }
 
@@ -364,39 +379,33 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
 
   resetTypeahead = () => {
     if (this.mounted) {
-      this.setState({
-        suggestions: [],
-        typeaheadIndex: 0,
-        typeaheadPrefix: '',
-        typeaheadContext: null,
-      });
+      this.setState({ suggestions: [], typeaheadIndex: 0, typeaheadPrefix: '', typeaheadContext: null });
       this.resetTimer = null;
     }
   };
 
-  handleBlur = () => {
-    const { onBlur } = this.props;
+  handleBlur = (event, change) => {
+    const { lastExecutedValue } = this.state;
+    const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null;
+    const currentValue = Plain.serialize(change.value);
+
     // If we dont wait here, menu clicks wont work because the menu
     // will be gone.
     this.resetTimer = setTimeout(this.resetTypeahead, 100);
     // Disrupting placeholder entry wipes all remaining placeholders needing input
     this.placeholdersBuffer.clearPlaceholders();
-    if (onBlur) {
-      onBlur();
-    }
-  };
 
-  handleFocus = () => {
-    const { onFocus } = this.props;
-    if (onFocus) {
-      onFocus();
+    if (previousValue !== currentValue) {
+      this.executeOnQueryChangeAndExecuteQueries();
     }
   };
 
+  handleFocus = () => {};
+
   onClickMenu = (item: CompletionItem) => {
     // Manually triggering change
     const change = this.applyTypeahead(this.state.value.change(), item);
-    this.onChange(change);
+    this.onChange(change, true);
   };
 
   updateMenu = () => {
@@ -459,6 +468,14 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     );
   };
 
+  handlePaste = (event: ClipboardEvent, change: Editor) => {
+    const pastedValue = event.clipboardData.getData('Text');
+    const newValue = change.value.change().insertText(pastedValue);
+    this.onChange(newValue);
+
+    return true;
+  };
+
   render() {
     const { disabled } = this.props;
     const wrapperClassName = classnames('slate-query-field__wrapper', {
@@ -475,6 +492,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
             onKeyDown={this.onKeyDown}
             onChange={this.onChange}
             onFocus={this.handleFocus}
+            onPaste={this.handlePaste}
             placeholder={this.props.placeholder}
             plugins={this.plugins}
             spellCheck={false}

+ 24 - 29
public/app/features/explore/QueryRow.tsx

@@ -9,20 +9,14 @@ import QueryEditor from './QueryEditor';
 import QueryTransactionStatus from './QueryTransactionStatus';
 
 // Actions
-import {
-  addQueryRow,
-  changeQuery,
-  highlightLogsExpression,
-  modifyQueries,
-  removeQueryRow,
-  runQueries,
-} from './state/actions';
+import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions';
 
 // Types
 import { StoreState } from 'app/types';
-import { RawTimeRange, DataQuery, QueryHint } from '@grafana/ui';
+import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui';
 import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
+import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
 
 function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
   const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@@ -37,16 +31,16 @@ interface QueryRowProps {
   changeQuery: typeof changeQuery;
   className?: string;
   exploreId: ExploreId;
-  datasourceInstance: any;
-  highlightLogsExpression: typeof highlightLogsExpression;
+  datasourceInstance: ExploreDataSourceApi;
+  highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
   history: HistoryItem[];
   index: number;
-  initialQuery: DataQuery;
+  query: DataQuery;
   modifyQueries: typeof modifyQueries;
   queryTransactions: QueryTransaction[];
   exploreEvents: Emitter;
   range: RawTimeRange;
-  removeQueryRow: typeof removeQueryRow;
+  removeQueryRowAction: typeof removeQueryRowAction;
   runQueries: typeof runQueries;
 }
 
@@ -78,29 +72,30 @@ export class QueryRow extends PureComponent<QueryRowProps> {
     this.onChangeQuery(null, true);
   };
 
-  onClickHintFix = action => {
+  onClickHintFix = (action: QueryFixAction) => {
     const { datasourceInstance, exploreId, index } = this.props;
     if (datasourceInstance && datasourceInstance.modifyQuery) {
-      const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
+      const modifier = (queries: DataQuery, action: QueryFixAction) => datasourceInstance.modifyQuery(queries, action);
       this.props.modifyQueries(exploreId, action, index, modifier);
     }
   };
 
   onClickRemoveButton = () => {
     const { exploreId, index } = this.props;
-    this.props.removeQueryRow(exploreId, index);
+    this.props.removeQueryRowAction({ exploreId, index });
   };
 
   updateLogsHighlights = _.debounce((value: DataQuery) => {
     const { datasourceInstance } = this.props;
     if (datasourceInstance.getHighlighterExpression) {
+      const { exploreId } = this.props;
       const expressions = [datasourceInstance.getHighlighterExpression(value)];
-      this.props.highlightLogsExpression(this.props.exploreId, expressions);
+      this.props.highlightLogsExpressionAction({ exploreId, expressions });
     }
   }, 500);
 
   render() {
-    const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
+    const { datasourceInstance, history, index, query, queryTransactions, exploreEvents, range } = this.props;
     const transactions = queryTransactions.filter(t => t.rowIndex === index);
     const transactionWithError = transactions.find(t => t.error !== undefined);
     const hint = getFirstHintFromTransactions(transactions);
@@ -111,16 +106,16 @@ export class QueryRow extends PureComponent<QueryRowProps> {
         <div className="query-row-status">
           <QueryTransactionStatus transactions={transactions} />
         </div>
-        <div className="query-row-field">
+        <div className="query-row-field flex-shrink-1">
           {QueryField ? (
             <QueryField
               datasource={datasourceInstance}
+              query={query}
               error={queryError}
               hint={hint}
-              initialQuery={initialQuery}
               history={history}
-              onClickHintFix={this.onClickHintFix}
-              onPressEnter={this.onExecuteQuery}
+              onExecuteQuery={this.onExecuteQuery}
+              onExecuteHint={this.onClickHintFix}
               onQueryChange={this.onChangeQuery}
             />
           ) : (
@@ -129,13 +124,13 @@ export class QueryRow extends PureComponent<QueryRowProps> {
               error={queryError}
               onQueryChange={this.onChangeQuery}
               onExecuteQuery={this.onExecuteQuery}
-              initialQuery={initialQuery}
+              initialQuery={query}
               exploreEvents={exploreEvents}
               range={range}
             />
           )}
         </div>
-        <div className="gf-form-inline">
+        <div className="gf-form-inline flex-shrink-0">
           <div className="gf-form">
             <button className="gf-form-label gf-form-label--btn" onClick={this.onClickClearButton}>
               <i className="fa fa-times" />
@@ -160,17 +155,17 @@ export class QueryRow extends PureComponent<QueryRowProps> {
 function mapStateToProps(state: StoreState, { exploreId, index }) {
   const explore = state.explore;
   const item: ExploreItemState = explore[exploreId];
-  const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
-  const initialQuery = initialQueries[index];
-  return { datasourceInstance, history, initialQuery, queryTransactions, range };
+  const { datasourceInstance, history, queries, queryTransactions, range } = item;
+  const query = queries[index];
+  return { datasourceInstance, history, query, queryTransactions, range };
 }
 
 const mapDispatchToProps = {
   addQueryRow,
   changeQuery,
-  highlightLogsExpression,
+  highlightLogsExpressionAction,
   modifyQueries,
-  removeQueryRow,
+  removeQueryRowAction,
   runQueries,
 };
 

+ 5 - 7
public/app/features/explore/QueryRows.tsx

@@ -6,25 +6,23 @@ import QueryRow from './QueryRow';
 
 // Types
 import { Emitter } from 'app/core/utils/emitter';
-import { DataQuery } from '@grafana/ui/src/types';
 import { ExploreId } from 'app/types/explore';
 
 interface QueryRowsProps {
   className?: string;
   exploreEvents: Emitter;
   exploreId: ExploreId;
-  initialQueries: DataQuery[];
+  queryKeys: string[];
 }
 
 export default class QueryRows extends PureComponent<QueryRowsProps> {
   render() {
-    const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
+    const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
     return (
       <div className={className}>
-        {initialQueries.map((query, index) => (
-          // TODO instead of relying on initialQueries, move to react key list in redux
-          <QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
-        ))}
+        {queryKeys.map((key, index) => {
+          return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
+        })}
       </div>
     );
   }

+ 7 - 7
public/app/features/explore/Wrapper.tsx

@@ -7,16 +7,16 @@ import { StoreState } from 'app/types';
 import { ExploreId, ExploreUrlState } from 'app/types/explore';
 import { parseUrlState } from 'app/core/utils/explore';
 
-import { initializeExploreSplit, resetExplore } from './state/actions';
 import ErrorBoundary from './ErrorBoundary';
 import Explore from './Explore';
 import { CustomScrollbar } from '@grafana/ui';
+import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
 
 interface WrapperProps {
-  initializeExploreSplit: typeof initializeExploreSplit;
+  initializeExploreSplitAction: typeof initializeExploreSplitAction;
   split: boolean;
   updateLocation: typeof updateLocation;
-  resetExplore: typeof resetExplore;
+  resetExploreAction: typeof resetExploreAction;
   urlStates: { [key: string]: string };
 }
 
@@ -39,12 +39,12 @@ export class Wrapper extends Component<WrapperProps> {
 
   componentDidMount() {
     if (this.initialSplit) {
-      this.props.initializeExploreSplit();
+      this.props.initializeExploreSplitAction();
     }
   }
 
   componentWillUnmount() {
-    this.props.resetExplore();
+    this.props.resetExploreAction();
   }
 
   render() {
@@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => {
 };
 
 const mapDispatchToProps = {
-  initializeExploreSplit,
+  initializeExploreSplitAction,
   updateLocation,
-  resetExplore,
+  resetExploreAction,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));

+ 3 - 3
public/app/features/explore/__snapshots__/Graph.test.tsx.snap

@@ -7,7 +7,7 @@ exports[`Render should render component 1`] = `
     id="graph"
     style={
       Object {
-        "height": "100px",
+        "height": 100,
       }
     }
   />
@@ -480,7 +480,7 @@ exports[`Render should render component with disclaimer 1`] = `
     id="graph"
     style={
       Object {
-        "height": "100px",
+        "height": 100,
       }
     }
   />
@@ -962,7 +962,7 @@ exports[`Render should show query return no time series 1`] = `
     id="graph"
     style={
       Object {
-        "height": "100px",
+        "height": 100,
       }
     }
   />

+ 344 - 238
public/app/features/explore/state/actionTypes.ts

@@ -1,6 +1,13 @@
 // Types
 import { Emitter } from 'app/core/core';
-import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi } from '@grafana/ui/src/types';
+import {
+  RawTimeRange,
+  TimeRange,
+  DataQuery,
+  DataSourceSelectItem,
+  DataSourceApi,
+  QueryFixAction,
+} from '@grafana/ui/src/types';
 import {
   ExploreId,
   ExploreItemState,
@@ -8,317 +15,416 @@ import {
   RangeScanner,
   ResultType,
   QueryTransaction,
+  ExploreUIState,
 } from 'app/types/explore';
+import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
 
+/**  Higher order actions
+ *
+ */
 export enum ActionTypes {
-  AddQueryRow = 'explore/ADD_QUERY_ROW',
-  ChangeDatasource = 'explore/CHANGE_DATASOURCE',
-  ChangeQuery = 'explore/CHANGE_QUERY',
-  ChangeSize = 'explore/CHANGE_SIZE',
-  ChangeTime = 'explore/CHANGE_TIME',
-  ClearQueries = 'explore/CLEAR_QUERIES',
-  HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
-  InitializeExplore = 'explore/INITIALIZE_EXPLORE',
   InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
-  LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
-  LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
-  LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
-  LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
-  ModifyQueries = 'explore/MODIFY_QUERIES',
-  QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
-  QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
-  QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
-  RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
-  RunQueries = 'explore/RUN_QUERIES',
-  RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
-  ScanRange = 'explore/SCAN_RANGE',
-  ScanStart = 'explore/SCAN_START',
-  ScanStop = 'explore/SCAN_STOP',
-  SetQueries = 'explore/SET_QUERIES',
   SplitClose = 'explore/SPLIT_CLOSE',
   SplitOpen = 'explore/SPLIT_OPEN',
-  StateSave = 'explore/STATE_SAVE',
-  ToggleGraph = 'explore/TOGGLE_GRAPH',
-  ToggleLogs = 'explore/TOGGLE_LOGS',
-  ToggleTable = 'explore/TOGGLE_TABLE',
-  UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE',
   ResetExplore = 'explore/RESET_EXPLORE',
-  QueriesImported = 'explore/QueriesImported',
 }
 
-export interface AddQueryRowAction {
-  type: ActionTypes.AddQueryRow;
-  payload: {
-    exploreId: ExploreId;
-    index: number;
-    query: DataQuery;
-  };
+export interface InitializeExploreSplitAction {
+  type: ActionTypes.InitializeExploreSplit;
+  payload: {};
 }
 
-export interface ChangeQueryAction {
-  type: ActionTypes.ChangeQuery;
-  payload: {
-    exploreId: ExploreId;
-    query: DataQuery;
-    index: number;
-    override: boolean;
-  };
+export interface SplitCloseAction {
+  type: ActionTypes.SplitClose;
+  payload: {};
 }
 
-export interface ChangeSizeAction {
-  type: ActionTypes.ChangeSize;
+export interface SplitOpenAction {
+  type: ActionTypes.SplitOpen;
   payload: {
-    exploreId: ExploreId;
-    width: number;
-    height: number;
+    itemState: ExploreItemState;
   };
 }
 
-export interface ChangeTimeAction {
-  type: ActionTypes.ChangeTime;
-  payload: {
-    exploreId: ExploreId;
-    range: TimeRange;
-  };
+export interface ResetExploreAction {
+  type: ActionTypes.ResetExplore;
+  payload: {};
 }
 
-export interface ClearQueriesAction {
-  type: ActionTypes.ClearQueries;
-  payload: {
-    exploreId: ExploreId;
-  };
+/**  Lower order actions
+ *
+ */
+export interface AddQueryRowPayload {
+  exploreId: ExploreId;
+  index: number;
+  query: DataQuery;
 }
 
-export interface HighlightLogsExpressionAction {
-  type: ActionTypes.HighlightLogsExpression;
-  payload: {
-    exploreId: ExploreId;
-    expressions: string[];
-  };
+export interface ChangeQueryPayload {
+  exploreId: ExploreId;
+  query: DataQuery;
+  index: number;
+  override: boolean;
 }
 
-export interface InitializeExploreAction {
-  type: ActionTypes.InitializeExplore;
-  payload: {
-    exploreId: ExploreId;
-    containerWidth: number;
-    eventBridge: Emitter;
-    exploreDatasources: DataSourceSelectItem[];
-    queries: DataQuery[];
-    range: RawTimeRange;
-  };
+export interface ChangeSizePayload {
+  exploreId: ExploreId;
+  width: number;
+  height: number;
 }
 
-export interface InitializeExploreSplitAction {
-  type: ActionTypes.InitializeExploreSplit;
+export interface ChangeTimePayload {
+  exploreId: ExploreId;
+  range: TimeRange;
 }
 
-export interface LoadDatasourceFailureAction {
-  type: ActionTypes.LoadDatasourceFailure;
-  payload: {
-    exploreId: ExploreId;
-    error: string;
-  };
+export interface ClearQueriesPayload {
+  exploreId: ExploreId;
 }
 
-export interface LoadDatasourcePendingAction {
-  type: ActionTypes.LoadDatasourcePending;
-  payload: {
-    exploreId: ExploreId;
-    requestedDatasourceName: string;
-  };
+export interface HighlightLogsExpressionPayload {
+  exploreId: ExploreId;
+  expressions: string[];
 }
 
-export interface LoadDatasourceMissingAction {
-  type: ActionTypes.LoadDatasourceMissing;
-  payload: {
-    exploreId: ExploreId;
-  };
+export interface InitializeExplorePayload {
+  exploreId: ExploreId;
+  containerWidth: number;
+  eventBridge: Emitter;
+  exploreDatasources: DataSourceSelectItem[];
+  queries: DataQuery[];
+  range: RawTimeRange;
+  ui: ExploreUIState;
 }
 
-export interface LoadDatasourceSuccessAction {
-  type: ActionTypes.LoadDatasourceSuccess;
-  payload: {
-    exploreId: ExploreId;
-    StartPage?: any;
-    datasourceInstance: any;
-    history: HistoryItem[];
-    logsHighlighterExpressions?: any[];
-    showingStartPage: boolean;
-    supportsGraph: boolean;
-    supportsLogs: boolean;
-    supportsTable: boolean;
-  };
+export interface LoadDatasourceFailurePayload {
+  exploreId: ExploreId;
+  error: string;
 }
 
-export interface ModifyQueriesAction {
-  type: ActionTypes.ModifyQueries;
-  payload: {
-    exploreId: ExploreId;
-    modification: any;
-    index: number;
-    modifier: (queries: DataQuery[], modification: any) => DataQuery[];
-  };
+export interface LoadDatasourceMissingPayload {
+  exploreId: ExploreId;
 }
 
-export interface QueryTransactionFailureAction {
-  type: ActionTypes.QueryTransactionFailure;
-  payload: {
-    exploreId: ExploreId;
-    queryTransactions: QueryTransaction[];
-  };
+export interface LoadDatasourcePendingPayload {
+  exploreId: ExploreId;
+  requestedDatasourceName: string;
 }
 
-export interface QueryTransactionStartAction {
-  type: ActionTypes.QueryTransactionStart;
-  payload: {
-    exploreId: ExploreId;
-    resultType: ResultType;
-    rowIndex: number;
-    transaction: QueryTransaction;
-  };
+export interface LoadDatasourceSuccessPayload {
+  exploreId: ExploreId;
+  StartPage?: any;
+  datasourceInstance: any;
+  history: HistoryItem[];
+  logsHighlighterExpressions?: any[];
+  showingStartPage: boolean;
+  supportsGraph: boolean;
+  supportsLogs: boolean;
+  supportsTable: boolean;
 }
 
-export interface QueryTransactionSuccessAction {
-  type: ActionTypes.QueryTransactionSuccess;
-  payload: {
-    exploreId: ExploreId;
-    history: HistoryItem[];
-    queryTransactions: QueryTransaction[];
-  };
+export interface ModifyQueriesPayload {
+  exploreId: ExploreId;
+  modification: QueryFixAction;
+  index: number;
+  modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery;
 }
 
-export interface RemoveQueryRowAction {
-  type: ActionTypes.RemoveQueryRow;
-  payload: {
-    exploreId: ExploreId;
-    index: number;
-  };
+export interface QueryTransactionFailurePayload {
+  exploreId: ExploreId;
+  queryTransactions: QueryTransaction[];
 }
 
-export interface RunQueriesEmptyAction {
-  type: ActionTypes.RunQueriesEmpty;
-  payload: {
-    exploreId: ExploreId;
-  };
+export interface QueryTransactionStartPayload {
+  exploreId: ExploreId;
+  resultType: ResultType;
+  rowIndex: number;
+  transaction: QueryTransaction;
 }
 
-export interface ScanStartAction {
-  type: ActionTypes.ScanStart;
-  payload: {
-    exploreId: ExploreId;
-    scanner: RangeScanner;
-  };
+export interface QueryTransactionSuccessPayload {
+  exploreId: ExploreId;
+  history: HistoryItem[];
+  queryTransactions: QueryTransaction[];
 }
 
-export interface ScanRangeAction {
-  type: ActionTypes.ScanRange;
-  payload: {
-    exploreId: ExploreId;
-    range: RawTimeRange;
-  };
+export interface RemoveQueryRowPayload {
+  exploreId: ExploreId;
+  index: number;
 }
 
-export interface ScanStopAction {
-  type: ActionTypes.ScanStop;
-  payload: {
-    exploreId: ExploreId;
-  };
+export interface RunQueriesEmptyPayload {
+  exploreId: ExploreId;
 }
 
-export interface SetQueriesAction {
-  type: ActionTypes.SetQueries;
-  payload: {
-    exploreId: ExploreId;
-    queries: DataQuery[];
-  };
+export interface ScanStartPayload {
+  exploreId: ExploreId;
+  scanner: RangeScanner;
 }
 
-export interface SplitCloseAction {
-  type: ActionTypes.SplitClose;
+export interface ScanRangePayload {
+  exploreId: ExploreId;
+  range: RawTimeRange;
 }
 
-export interface SplitOpenAction {
-  type: ActionTypes.SplitOpen;
-  payload: {
-    itemState: ExploreItemState;
-  };
+export interface ScanStopPayload {
+  exploreId: ExploreId;
 }
 
-export interface StateSaveAction {
-  type: ActionTypes.StateSave;
+export interface SetQueriesPayload {
+  exploreId: ExploreId;
+  queries: DataQuery[];
 }
 
-export interface ToggleTableAction {
-  type: ActionTypes.ToggleTable;
-  payload: {
-    exploreId: ExploreId;
-  };
+export interface SplitOpenPayload {
+  itemState: ExploreItemState;
 }
 
-export interface ToggleGraphAction {
-  type: ActionTypes.ToggleGraph;
-  payload: {
-    exploreId: ExploreId;
-  };
+export interface ToggleTablePayload {
+  exploreId: ExploreId;
 }
 
-export interface ToggleLogsAction {
-  type: ActionTypes.ToggleLogs;
-  payload: {
-    exploreId: ExploreId;
-  };
+export interface ToggleGraphPayload {
+  exploreId: ExploreId;
 }
 
-export interface UpdateDatasourceInstanceAction {
-  type: ActionTypes.UpdateDatasourceInstance;
-  payload: {
-    exploreId: ExploreId;
-    datasourceInstance: DataSourceApi;
-  };
+export interface ToggleLogsPayload {
+  exploreId: ExploreId;
 }
 
-export interface ResetExploreAction {
-  type: ActionTypes.ResetExplore;
-  payload: {};
+export interface UpdateDatasourceInstancePayload {
+  exploreId: ExploreId;
+  datasourceInstance: DataSourceApi;
 }
 
-export interface QueriesImported {
-  type: ActionTypes.QueriesImported;
-  payload: {
-    exploreId: ExploreId;
-    queries: DataQuery[];
-  };
+export interface QueriesImportedPayload {
+  exploreId: ExploreId;
+  queries: DataQuery[];
 }
 
-export type Action =
-  | AddQueryRowAction
-  | ChangeQueryAction
-  | ChangeSizeAction
-  | ChangeTimeAction
-  | ClearQueriesAction
-  | HighlightLogsExpressionAction
-  | InitializeExploreAction
+/**
+ * Adds a query row after the row with the given index.
+ */
+export const addQueryRowAction = actionCreatorFactory<AddQueryRowPayload>('explore/ADD_QUERY_ROW').create();
+
+/**
+ * Loads a new datasource identified by the given name.
+ */
+export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create();
+
+/**
+ * Query change handler for the query row with the given index.
+ * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
+ */
+export const changeQueryAction = actionCreatorFactory<ChangeQueryPayload>('explore/CHANGE_QUERY').create();
+
+/**
+ * Keep track of the Explore container size, in particular the width.
+ * The width will be used to calculate graph intervals (number of datapoints).
+ */
+export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore/CHANGE_SIZE').create();
+
+/**
+ * Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
+ */
+export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
+
+/**
+ * Clear all queries and results.
+ */
+export const clearQueriesAction = actionCreatorFactory<ClearQueriesPayload>('explore/CLEAR_QUERIES').create();
+
+/**
+ * Highlight expressions in the log results
+ */
+export const highlightLogsExpressionAction = actionCreatorFactory<HighlightLogsExpressionPayload>(
+  'explore/HIGHLIGHT_LOGS_EXPRESSION'
+).create();
+
+/**
+ * Initialize Explore state with state from the URL and the React component.
+ * Call this only on components for with the Explore state has not been initialized.
+ */
+export const initializeExploreAction = actionCreatorFactory<InitializeExplorePayload>(
+  'explore/INITIALIZE_EXPLORE'
+).create();
+
+/**
+ * Initialize the wrapper split state
+ */
+export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create();
+
+/**
+ * Display an error that happened during the selection of a datasource
+ */
+export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>(
+  'explore/LOAD_DATASOURCE_FAILURE'
+).create();
+
+/**
+ * Display an error when no datasources have been configured
+ */
+export const loadDatasourceMissingAction = actionCreatorFactory<LoadDatasourceMissingPayload>(
+  'explore/LOAD_DATASOURCE_MISSING'
+).create();
+
+/**
+ * Start the async process of loading a datasource to display a loading indicator
+ */
+export const loadDatasourcePendingAction = actionCreatorFactory<LoadDatasourcePendingPayload>(
+  'explore/LOAD_DATASOURCE_PENDING'
+).create();
+
+/**
+ * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
+ * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
+ * e.g., Prometheus -> Loki queries.
+ */
+export const loadDatasourceSuccessAction = actionCreatorFactory<LoadDatasourceSuccessPayload>(
+  'explore/LOAD_DATASOURCE_SUCCESS'
+).create();
+
+/**
+ * Action to modify a query given a datasource-specific modifier action.
+ * @param exploreId Explore area
+ * @param modification Action object with a type, e.g., ADD_FILTER
+ * @param index Optional query row index. If omitted, the modification is applied to all query rows.
+ * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
+ */
+export const modifyQueriesAction = actionCreatorFactory<ModifyQueriesPayload>('explore/MODIFY_QUERIES').create();
+
+/**
+ * Mark a query transaction as failed with an error extracted from the query response.
+ * The transaction will be marked as `done`.
+ */
+export const queryTransactionFailureAction = actionCreatorFactory<QueryTransactionFailurePayload>(
+  'explore/QUERY_TRANSACTION_FAILURE'
+).create();
+
+/**
+ * Start a query transaction for the given result type.
+ * @param exploreId Explore area
+ * @param transaction Query options and `done` status.
+ * @param resultType Associate the transaction with a result viewer, e.g., Graph
+ * @param rowIndex Index is used to associate latency for this transaction with a query row
+ */
+export const queryTransactionStartAction = actionCreatorFactory<QueryTransactionStartPayload>(
+  'explore/QUERY_TRANSACTION_START'
+).create();
+
+/**
+ * Complete a query transaction, mark the transaction as `done` and store query state in URL.
+ * If the transaction was started by a scanner, it keeps on scanning for more results.
+ * Side-effect: the query is stored in localStorage.
+ * @param exploreId Explore area
+ * @param transactionId ID
+ * @param result Response from `datasourceInstance.query()`
+ * @param latency Duration between request and response
+ * @param queries Queries from all query rows
+ * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
+ */
+export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransactionSuccessPayload>(
+  'explore/QUERY_TRANSACTION_SUCCESS'
+).create();
+
+/**
+ * Remove query row of the given index, as well as associated query results.
+ */
+export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
+export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create();
+export const runQueriesEmptyAction = actionCreatorFactory<RunQueriesEmptyPayload>('explore/RUN_QUERIES_EMPTY').create();
+
+/**
+ * Start a scan for more results using the given scanner.
+ * @param exploreId Explore area
+ * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
+ */
+export const scanStartAction = actionCreatorFactory<ScanStartPayload>('explore/SCAN_START').create();
+export const scanRangeAction = actionCreatorFactory<ScanRangePayload>('explore/SCAN_RANGE').create();
+
+/**
+ * Stop any scanning for more results.
+ */
+export const scanStopAction = actionCreatorFactory<ScanStopPayload>('explore/SCAN_STOP').create();
+
+/**
+ * Reset queries to the given queries. Any modifications will be discarded.
+ * Use this action for clicks on query examples. Triggers a query run.
+ */
+export const setQueriesAction = actionCreatorFactory<SetQueriesPayload>('explore/SET_QUERIES').create();
+
+/**
+ * Close the split view and save URL state.
+ */
+export const splitCloseAction = noPayloadActionCreatorFactory('explore/SPLIT_CLOSE').create();
+
+/**
+ * Open the split view and copy the left state to be the right state.
+ * The right state is automatically initialized.
+ * The copy keeps all query modifications but wipes the query results.
+ */
+export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/SPLIT_OPEN').create();
+export const stateSaveAction = noPayloadActionCreatorFactory('explore/STATE_SAVE').create();
+
+/**
+ * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
+ */
+export const toggleTableAction = actionCreatorFactory<ToggleTablePayload>('explore/TOGGLE_TABLE').create();
+
+/**
+ * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
+ */
+export const toggleGraphAction = actionCreatorFactory<ToggleGraphPayload>('explore/TOGGLE_GRAPH').create();
+
+/**
+ * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
+ */
+export const toggleLogsAction = actionCreatorFactory<ToggleLogsPayload>('explore/TOGGLE_LOGS').create();
+
+/**
+ * Updates datasource instance before datasouce loading has started
+ */
+export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasourceInstancePayload>(
+  'explore/UPDATE_DATASOURCE_INSTANCE'
+).create();
+
+/**
+ * Resets state for explore.
+ */
+export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
+export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
+
+export type HigherOrderAction =
   | InitializeExploreSplitAction
-  | LoadDatasourceFailureAction
-  | LoadDatasourceMissingAction
-  | LoadDatasourcePendingAction
-  | LoadDatasourceSuccessAction
-  | ModifyQueriesAction
-  | QueryTransactionFailureAction
-  | QueryTransactionStartAction
-  | QueryTransactionSuccessAction
-  | RemoveQueryRowAction
-  | RunQueriesEmptyAction
-  | ScanRangeAction
-  | ScanStartAction
-  | ScanStopAction
-  | SetQueriesAction
   | SplitCloseAction
   | SplitOpenAction
-  | ToggleGraphAction
-  | ToggleLogsAction
-  | ToggleTableAction
-  | UpdateDatasourceInstanceAction
   | ResetExploreAction
-  | QueriesImported;
+  | ActionOf<any>;
+
+export type Action =
+  | ActionOf<AddQueryRowPayload>
+  | ActionOf<ChangeQueryPayload>
+  | ActionOf<ChangeSizePayload>
+  | ActionOf<ChangeTimePayload>
+  | ActionOf<ClearQueriesPayload>
+  | ActionOf<HighlightLogsExpressionPayload>
+  | ActionOf<InitializeExplorePayload>
+  | ActionOf<LoadDatasourceFailurePayload>
+  | ActionOf<LoadDatasourceMissingPayload>
+  | ActionOf<LoadDatasourcePendingPayload>
+  | ActionOf<LoadDatasourceSuccessPayload>
+  | ActionOf<ModifyQueriesPayload>
+  | ActionOf<QueryTransactionFailurePayload>
+  | ActionOf<QueryTransactionStartPayload>
+  | ActionOf<QueryTransactionSuccessPayload>
+  | ActionOf<RemoveQueryRowPayload>
+  | ActionOf<RunQueriesEmptyPayload>
+  | ActionOf<ScanStartPayload>
+  | ActionOf<ScanRangePayload>
+  | ActionOf<SetQueriesPayload>
+  | ActionOf<SplitOpenPayload>
+  | ActionOf<ToggleTablePayload>
+  | ActionOf<ToggleGraphPayload>
+  | ActionOf<ToggleLogsPayload>
+  | ActionOf<UpdateDatasourceInstancePayload>
+  | ActionOf<QueriesImportedPayload>;

+ 162 - 252
public/app/features/explore/state/actions.ts

@@ -30,40 +30,54 @@ import {
   DataQuery,
   DataSourceSelectItem,
   QueryHint,
+  QueryFixAction,
 } from '@grafana/ui/src/types';
+import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions, ExploreUIState } from 'app/types/explore';
 import {
-  ExploreId,
-  ExploreUrlState,
-  RangeScanner,
-  ResultType,
-  QueryOptions,
-  QueryTransaction,
-} from 'app/types/explore';
-
-import {
-  Action as ThunkableAction,
-  ActionTypes,
-  AddQueryRowAction,
-  ChangeSizeAction,
-  HighlightLogsExpressionAction,
-  LoadDatasourceFailureAction,
-  LoadDatasourceMissingAction,
-  LoadDatasourcePendingAction,
-  LoadDatasourceSuccessAction,
-  QueryTransactionStartAction,
-  ScanStopAction,
-  UpdateDatasourceInstanceAction,
-  QueriesImported,
+  Action,
+  updateDatasourceInstanceAction,
+  changeQueryAction,
+  changeSizeAction,
+  ChangeSizePayload,
+  changeTimeAction,
+  scanStopAction,
+  clearQueriesAction,
+  initializeExploreAction,
+  loadDatasourceMissingAction,
+  loadDatasourceFailureAction,
+  loadDatasourcePendingAction,
+  queriesImportedAction,
+  LoadDatasourceSuccessPayload,
+  loadDatasourceSuccessAction,
+  modifyQueriesAction,
+  queryTransactionFailureAction,
+  queryTransactionStartAction,
+  queryTransactionSuccessAction,
+  scanRangeAction,
+  runQueriesEmptyAction,
+  scanStartAction,
+  setQueriesAction,
+  splitCloseAction,
+  splitOpenAction,
+  addQueryRowAction,
+  AddQueryRowPayload,
+  toggleGraphAction,
+  toggleLogsAction,
+  toggleTableAction,
+  ToggleGraphPayload,
+  ToggleLogsPayload,
+  ToggleTablePayload,
 } from './actionTypes';
+import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
 
-type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
-/**
- * Adds a query row after the row with the given index.
- */
-export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
+// /**
+//  * Adds a query row after the row with the given index.
+//  */
+export function addQueryRow(exploreId: ExploreId, index: number): ActionOf<AddQueryRowPayload> {
   const query = generateEmptyQuery(index + 1);
-  return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
+  return addQueryRowAction({ exploreId, index, query });
 }
 
 /**
@@ -73,12 +87,20 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
   return async (dispatch, getState) => {
     const newDataSourceInstance = await getDatasourceSrv().get(datasource);
     const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
-    const modifiedQueries = getState().explore[exploreId].modifiedQueries;
+    const queries = getState().explore[exploreId].queries;
 
-    await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
+    await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
 
-    dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
-    dispatch(loadDatasource(exploreId, newDataSourceInstance));
+    dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
+
+    try {
+      await dispatch(loadDatasource(exploreId, newDataSourceInstance));
+    } catch (error) {
+      console.error(error);
+      return;
+    }
+
+    dispatch(runQueries(exploreId));
   };
 }
 
@@ -98,7 +120,7 @@ export function changeQuery(
       query = { ...generateEmptyQuery(index) };
     }
 
-    dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
+    dispatch(changeQueryAction({ exploreId, query, index, override }));
     if (override) {
       dispatch(runQueries(exploreId));
     }
@@ -112,8 +134,8 @@ export function changeQuery(
 export function changeSize(
   exploreId: ExploreId,
   { height, width }: { height: number; width: number }
-): ChangeSizeAction {
-  return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
+): ActionOf<ChangeSizePayload> {
+  return changeSizeAction({ exploreId, height, width });
 }
 
 /**
@@ -121,7 +143,7 @@ export function changeSize(
  */
 export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
   return dispatch => {
-    dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
+    dispatch(changeTimeAction({ exploreId, range }));
     dispatch(runQueries(exploreId));
   };
 }
@@ -131,19 +153,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
  */
 export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
   return dispatch => {
-    dispatch(scanStop(exploreId));
-    dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
+    dispatch(scanStopAction({ exploreId }));
+    dispatch(clearQueriesAction({ exploreId }));
     dispatch(stateSave());
   };
 }
 
-/**
- * Highlight expressions in the log results
- */
-export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
-  return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
-}
-
 /**
  * Initialize Explore state with state from the URL and the React component.
  * Call this only on components for with the Explore state has not been initialized.
@@ -154,7 +169,8 @@ export function initializeExplore(
   queries: DataQuery[],
   range: RawTimeRange,
   containerWidth: number,
-  eventBridge: Emitter
+  eventBridge: Emitter,
+  ui: ExploreUIState
 ): ThunkResult<void> {
   return async dispatch => {
     const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
@@ -165,18 +181,17 @@ export function initializeExplore(
         meta: ds.meta,
       }));
 
-    dispatch({
-      type: ActionTypes.InitializeExplore,
-      payload: {
+    dispatch(
+      initializeExploreAction({
         exploreId,
         containerWidth,
-        datasourceName,
         eventBridge,
         exploreDatasources,
         queries,
         range,
-      },
-    });
+        ui,
+      })
+    );
 
     if (exploreDatasources.length >= 1) {
       let instance;
@@ -193,75 +208,27 @@ export function initializeExplore(
         instance = await getDatasourceSrv().get();
       }
 
-      dispatch(updateDatasourceInstance(exploreId, instance));
-      dispatch(loadDatasource(exploreId, instance));
+      dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance }));
+
+      try {
+        await dispatch(loadDatasource(exploreId, instance));
+      } catch (error) {
+        console.error(error);
+        return;
+      }
+      dispatch(runQueries(exploreId, true));
     } else {
-      dispatch(loadDatasourceMissing(exploreId));
+      dispatch(loadDatasourceMissingAction({ exploreId }));
     }
   };
 }
 
-/**
- * Initialize the wrapper split state
- */
-export function initializeExploreSplit() {
-  return async dispatch => {
-    dispatch({ type: ActionTypes.InitializeExploreSplit });
-  };
-}
-
-/**
- * Display an error that happened during the selection of a datasource
- */
-export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
-  type: ActionTypes.LoadDatasourceFailure,
-  payload: {
-    exploreId,
-    error,
-  },
-});
-
-/**
- * Display an error when no datasources have been configured
- */
-export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
-  type: ActionTypes.LoadDatasourceMissing,
-  payload: { exploreId },
-});
-
-/**
- * Start the async process of loading a datasource to display a loading indicator
- */
-export const loadDatasourcePending = (
-  exploreId: ExploreId,
-  requestedDatasourceName: string
-): LoadDatasourcePendingAction => ({
-  type: ActionTypes.LoadDatasourcePending,
-  payload: {
-    exploreId,
-    requestedDatasourceName,
-  },
-});
-
-export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => {
-  return {
-    type: ActionTypes.QueriesImported,
-    payload: {
-      exploreId,
-      queries,
-    },
-  };
-};
-
 /**
  * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
  * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
  * e.g., Prometheus -> Loki queries.
  */
-export const loadDatasourceSuccess = (
-  exploreId: ExploreId,
-  instance: any,
-): LoadDatasourceSuccessAction => {
+export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf<LoadDatasourceSuccessPayload> => {
   // Capabilities
   const supportsGraph = instance.meta.metrics;
   const supportsLogs = instance.meta.logs;
@@ -274,37 +241,18 @@ export const loadDatasourceSuccess = (
   // Save last-used datasource
   store.set(LAST_USED_DATASOURCE_KEY, instance.name);
 
-  return {
-    type: ActionTypes.LoadDatasourceSuccess,
-    payload: {
-      exploreId,
-      StartPage,
-      datasourceInstance: instance,
-      history,
-      showingStartPage: Boolean(StartPage),
-      supportsGraph,
-      supportsLogs,
-      supportsTable,
-    },
-  };
+  return loadDatasourceSuccessAction({
+    exploreId,
+    StartPage,
+    datasourceInstance: instance,
+    history,
+    showingStartPage: Boolean(StartPage),
+    supportsGraph,
+    supportsLogs,
+    supportsTable,
+  });
 };
 
-/**
- * Updates datasource instance before datasouce loading has started
- */
-export function updateDatasourceInstance(
-  exploreId: ExploreId,
-  instance: DataSourceApi
-): UpdateDatasourceInstanceAction {
-  return {
-    type: ActionTypes.UpdateDatasourceInstance,
-    payload: {
-      exploreId,
-      datasourceInstance: instance,
-    },
-  };
-}
-
 export function importQueries(
   exploreId: ExploreId,
   queries: DataQuery[],
@@ -326,11 +274,11 @@ export function importQueries(
     }
 
     const nextQueries = importedQueries.map((q, i) => ({
-      ...importedQueries[i],
+      ...q,
       ...generateEmptyQuery(i),
     }));
 
-    dispatch(queriesImported(exploreId, nextQueries));
+    dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
   };
 }
 
@@ -342,9 +290,9 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
     const datasourceName = instance.name;
 
     // Keep ID to track selection
-    dispatch(loadDatasourcePending(exploreId, datasourceName));
-
+    dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
     let datasourceError = null;
+
     try {
       const testResult = await instance.testDatasource();
       datasourceError = testResult.status === 'success' ? null : testResult.message;
@@ -353,8 +301,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
     }
 
     if (datasourceError) {
-      dispatch(loadDatasourceFailure(exploreId, datasourceError));
-      return;
+      dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
+      return Promise.reject(`${datasourceName} loading failed`);
     }
 
     if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
@@ -372,7 +320,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
     }
 
     dispatch(loadDatasourceSuccess(exploreId, instance));
-    dispatch(runQueries(exploreId));
+    return Promise.resolve();
   };
 }
 
@@ -385,12 +333,12 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
  */
 export function modifyQueries(
   exploreId: ExploreId,
-  modification: any,
+  modification: QueryFixAction,
   index: number,
   modifier: any
 ): ThunkResult<void> {
   return dispatch => {
-    dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
+    dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
     if (!modification.preventSubmit) {
       dispatch(runQueries(exploreId));
     }
@@ -455,29 +403,10 @@ export function queryTransactionFailure(
       return qt;
     });
 
-    dispatch({
-      type: ActionTypes.QueryTransactionFailure,
-      payload: { exploreId, queryTransactions: nextQueryTransactions },
-    });
+    dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
   };
 }
 
-/**
- * Start a query transaction for the given result type.
- * @param exploreId Explore area
- * @param transaction Query options and `done` status.
- * @param resultType Associate the transaction with a result viewer, e.g., Graph
- * @param rowIndex Index is used to associate latency for this transaction with a query row
- */
-export function queryTransactionStart(
-  exploreId: ExploreId,
-  transaction: QueryTransaction,
-  resultType: ResultType,
-  rowIndex: number
-): QueryTransactionStartAction {
-  return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
-}
-
 /**
  * Complete a query transaction, mark the transaction as `done` and store query state in URL.
  * If the transaction was started by a scanner, it keeps on scanning for more results.
@@ -534,14 +463,13 @@ export function queryTransactionSuccess(
     // Side-effect: Saving history in localstorage
     const nextHistory = updateHistory(history, datasourceId, queries);
 
-    dispatch({
-      type: ActionTypes.QueryTransactionSuccess,
-      payload: {
+    dispatch(
+      queryTransactionSuccessAction({
         exploreId,
         history: nextHistory,
         queryTransactions: nextQueryTransactions,
-      },
-    });
+      })
+    );
 
     // Keep scanning for results if this was the last scanning transaction
     if (scanning) {
@@ -549,34 +477,24 @@ export function queryTransactionSuccess(
         const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
         if (!other) {
           const range = scanner();
-          dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
+          dispatch(scanRangeAction({ exploreId, range }));
         }
       } else {
         // We can stop scanning if we have a result
-        dispatch(scanStop(exploreId));
+        dispatch(scanStopAction({ exploreId }));
       }
     }
   };
 }
 
-/**
- * Remove query row of the given index, as well as associated query results.
- */
-export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
-  return dispatch => {
-    dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
-    dispatch(runQueries(exploreId));
-  };
-}
-
 /**
  * Main action to run queries and dispatches sub-actions based on which result viewers are active
  */
-export function runQueries(exploreId: ExploreId) {
+export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
   return (dispatch, getState) => {
     const {
       datasourceInstance,
-      modifiedQueries,
+      queries,
       showingLogs,
       showingGraph,
       showingTable,
@@ -585,8 +503,8 @@ export function runQueries(exploreId: ExploreId) {
       supportsTable,
     } = getState().explore[exploreId];
 
-    if (!hasNonEmptyQuery(modifiedQueries)) {
-      dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
+    if (!hasNonEmptyQuery(queries)) {
+      dispatch(runQueriesEmptyAction({ exploreId }));
       dispatch(stateSave()); // Remember to saves to state and update location
       return;
     }
@@ -596,7 +514,7 @@ export function runQueries(exploreId: ExploreId) {
     const interval = datasourceInstance.interval;
 
     // Keep table queries first since they need to return quickly
-    if (showingTable && supportsTable) {
+    if ((ignoreUIState || showingTable) && supportsTable) {
       dispatch(
         runQueriesForType(
           exploreId,
@@ -611,7 +529,7 @@ export function runQueries(exploreId: ExploreId) {
         )
       );
     }
-    if (showingGraph && supportsGraph) {
+    if ((ignoreUIState || showingGraph) && supportsGraph) {
       dispatch(
         runQueriesForType(
           exploreId,
@@ -625,9 +543,10 @@ export function runQueries(exploreId: ExploreId) {
         )
       );
     }
-    if (showingLogs && supportsLogs) {
+    if ((ignoreUIState || showingLogs) && supportsLogs) {
       dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
     }
+
     dispatch(stateSave());
   };
 }
@@ -646,14 +565,7 @@ function runQueriesForType(
   resultGetter?: any
 ) {
   return async (dispatch, getState) => {
-    const {
-      datasourceInstance,
-      eventBridge,
-      modifiedQueries: queries,
-      queryIntervals,
-      range,
-      scanning,
-    } = getState().explore[exploreId];
+    const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
     const datasourceId = datasourceInstance.meta.id;
 
     // Run all queries concurrently
@@ -667,7 +579,7 @@ function runQueriesForType(
         queryIntervals,
         scanning
       );
-      dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
+      dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
       try {
         const now = Date.now();
         const res = await datasourceInstance.query(transaction.options);
@@ -691,21 +603,14 @@ function runQueriesForType(
 export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
   return dispatch => {
     // Register the scanner
-    dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
+    dispatch(scanStartAction({ exploreId, scanner }));
     // Scanning must trigger query run, and return the new range
     const range = scanner();
     // Set the new range to be displayed
-    dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
+    dispatch(scanRangeAction({ exploreId, range }));
   };
 }
 
-/**
- * Stop any scanning for more results.
- */
-export function scanStop(exploreId: ExploreId): ScanStopAction {
-  return { type: ActionTypes.ScanStop, payload: { exploreId } };
-}
-
 /**
  * Reset queries to the given queries. Any modifications will be discarded.
  * Use this action for clicks on query examples. Triggers a query run.
@@ -714,13 +619,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
   return dispatch => {
     // Inject react keys into query objects
     const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
-    dispatch({
-      type: ActionTypes.SetQueries,
-      payload: {
-        exploreId,
-        queries,
-      },
-    });
+    dispatch(setQueriesAction({ exploreId, queries }));
     dispatch(runQueries(exploreId));
   };
 }
@@ -730,7 +629,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
  */
 export function splitClose(): ThunkResult<void> {
   return dispatch => {
-    dispatch({ type: ActionTypes.SplitClose });
+    dispatch(splitCloseAction());
     dispatch(stateSave());
   };
 }
@@ -747,9 +646,9 @@ export function splitOpen(): ThunkResult<void> {
     const itemState = {
       ...leftState,
       queryTransactions: [],
-      initialQueries: leftState.modifiedQueries.slice(),
+      queries: leftState.queries.slice(),
     };
-    dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
+    dispatch(splitOpenAction({ itemState }));
     dispatch(stateSave());
   };
 }
@@ -764,63 +663,74 @@ export function stateSave() {
     const urlStates: { [index: string]: string } = {};
     const leftUrlState: ExploreUrlState = {
       datasource: left.datasourceInstance.name,
-      queries: left.modifiedQueries.map(clearQueryKeys),
+      queries: left.queries.map(clearQueryKeys),
       range: left.range,
+      ui: {
+        showingGraph: left.showingGraph,
+        showingLogs: left.showingLogs,
+        showingTable: left.showingTable,
+      },
     };
     urlStates.left = serializeStateToUrlParam(leftUrlState, true);
     if (split) {
       const rightUrlState: ExploreUrlState = {
         datasource: right.datasourceInstance.name,
-        queries: right.modifiedQueries.map(clearQueryKeys),
+        queries: right.queries.map(clearQueryKeys),
         range: right.range,
+        ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable },
       };
+
       urlStates.right = serializeStateToUrlParam(rightUrlState, true);
     }
+
     dispatch(updateLocation({ query: urlStates }));
   };
 }
 
 /**
- * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
+ * Creates action to collapse graph/logs/table panel. When panel is collapsed,
+ * queries won't be run
  */
-export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
+const togglePanelActionCreator = (
+  actionCreator:
+    | ActionCreator<ToggleGraphPayload>
+    | ActionCreator<ToggleLogsPayload>
+    | ActionCreator<ToggleTablePayload>
+) => (exploreId: ExploreId) => {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
-    if (getState().explore[exploreId].showingGraph) {
+    let shouldRunQueries;
+    dispatch(actionCreator({ exploreId }));
+    dispatch(stateSave());
+
+    switch (actionCreator.type) {
+      case toggleGraphAction.type:
+        shouldRunQueries = getState().explore[exploreId].showingGraph;
+        break;
+      case toggleLogsAction.type:
+        shouldRunQueries = getState().explore[exploreId].showingLogs;
+        break;
+      case toggleTableAction.type:
+        shouldRunQueries = getState().explore[exploreId].showingTable;
+        break;
+    }
+
+    if (shouldRunQueries) {
       dispatch(runQueries(exploreId));
     }
   };
-}
+};
 
 /**
- * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
+ * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
  */
-export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
-  return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
-    if (getState().explore[exploreId].showingLogs) {
-      dispatch(runQueries(exploreId));
-    }
-  };
-}
+export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
 
 /**
- * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
+ * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
  */
-export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
-  return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
-    if (getState().explore[exploreId].showingTable) {
-      dispatch(runQueries(exploreId));
-    }
-  };
-}
+export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
 
 /**
- * Resets state for explore.
+ * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
  */
-export function resetExplore(): ThunkResult<void> {
-  return dispatch => {
-    dispatch({ type: ActionTypes.ResetExplore, payload: {} });
-  };
-}
+export const toggleTable = togglePanelActionCreator(toggleTableAction);

+ 35 - 30
public/app/features/explore/state/reducers.test.ts

@@ -1,42 +1,47 @@
-import { Action, ActionTypes } from './actionTypes';
 import { itemReducer, makeExploreItemState } from './reducers';
-import { ExploreId } from 'app/types/explore';
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { reducerTester } from 'test/core/redux/reducerTester';
+import { scanStartAction, scanStopAction } from './actionTypes';
+import { Reducer } from 'redux';
+import { ActionOf } from 'app/core/redux/actionCreatorFactory';
 
 describe('Explore item reducer', () => {
   describe('scanning', () => {
     test('should start scanning', () => {
-      let state = makeExploreItemState();
-      const action: Action = {
-        type: ActionTypes.ScanStart,
-        payload: {
-          exploreId: ExploreId.left,
-          scanner: jest.fn(),
-        },
+      const scanner = jest.fn();
+      const initalState = {
+        ...makeExploreItemState(),
+        scanning: false,
+        scanner: undefined,
       };
-      state = itemReducer(state, action);
-      expect(state.scanning).toBeTruthy();
-      expect(state.scanner).toBe(action.payload.scanner);
+
+      reducerTester()
+        .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
+        .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner }))
+        .thenStateShouldEqual({
+          ...makeExploreItemState(),
+          scanning: true,
+          scanner,
+        });
     });
     test('should stop scanning', () => {
-      let state = makeExploreItemState();
-      const start: Action = {
-        type: ActionTypes.ScanStart,
-        payload: {
-          exploreId: ExploreId.left,
-          scanner: jest.fn(),
-        },
-      };
-      state = itemReducer(state, start);
-      expect(state.scanning).toBeTruthy();
-      const action: Action = {
-        type: ActionTypes.ScanStop,
-        payload: {
-          exploreId: ExploreId.left,
-        },
+      const scanner = jest.fn();
+      const initalState = {
+        ...makeExploreItemState(),
+        scanning: true,
+        scanner,
+        scanRange: {},
       };
-      state = itemReducer(state, action);
-      expect(state.scanning).toBeFalsy();
-      expect(state.scanner).toBeUndefined();
+
+      reducerTester()
+        .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
+        .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
+        .thenStateShouldEqual({
+          ...makeExploreItemState(),
+          scanning: false,
+          scanner: undefined,
+          scanRange: undefined,
+        });
     });
   });
 });

+ 212 - 179
public/app/features/explore/state/reducers.ts

@@ -3,11 +3,41 @@ import {
   generateEmptyQuery,
   getIntervals,
   ensureQueries,
+  getQueryKeys,
 } from 'app/core/utils/explore';
 import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
 import { DataQuery } from '@grafana/ui/src/types';
 
-import { Action, ActionTypes } from './actionTypes';
+import { HigherOrderAction, ActionTypes } from './actionTypes';
+import { reducerFactory } from 'app/core/redux';
+import {
+  addQueryRowAction,
+  changeQueryAction,
+  changeSizeAction,
+  changeTimeAction,
+  clearQueriesAction,
+  highlightLogsExpressionAction,
+  initializeExploreAction,
+  updateDatasourceInstanceAction,
+  loadDatasourceFailureAction,
+  loadDatasourceMissingAction,
+  loadDatasourcePendingAction,
+  loadDatasourceSuccessAction,
+  modifyQueriesAction,
+  queryTransactionFailureAction,
+  queryTransactionStartAction,
+  queryTransactionSuccessAction,
+  removeQueryRowAction,
+  runQueriesEmptyAction,
+  scanRangeAction,
+  scanStartAction,
+  scanStopAction,
+  setQueriesAction,
+  toggleGraphAction,
+  toggleLogsAction,
+  toggleTableAction,
+  queriesImportedAction,
+} from './actionTypes';
 
 export const DEFAULT_RANGE = {
   from: 'now-6h',
@@ -30,9 +60,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
   datasourceMissing: false,
   exploreDatasources: [],
   history: [],
-  initialQueries: [],
+  queries: [],
   initialized: false,
-  modifiedQueries: [],
   queryTransactions: [],
   queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
   range: DEFAULT_RANGE,
@@ -44,6 +73,7 @@ export const makeExploreItemState = (): ExploreItemState => ({
   supportsGraph: null,
   supportsLogs: null,
   supportsTable: null,
+  queryKeys: [],
 });
 
 /**
@@ -58,21 +88,15 @@ export const initialExploreState: ExploreState = {
 /**
  * Reducer for an Explore area, to be used by the global Explore reducer.
  */
-export const itemReducer = (state, action: Action): ExploreItemState => {
-  switch (action.type) {
-    case ActionTypes.AddQueryRow: {
-      const { initialQueries, modifiedQueries, queryTransactions } = state;
+export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
+  .addMapper({
+    filter: addQueryRowAction,
+    mapper: (state, action): ExploreItemState => {
+      const { queries, queryTransactions } = state;
       const { index, query } = action.payload;
 
-      // Add new query row after given index, keep modifications of existing rows
-      const nextModifiedQueries = [
-        ...modifiedQueries.slice(0, index + 1),
-        { ...query },
-        ...initialQueries.slice(index + 1),
-      ];
-
-      // Add to initialQueries, which will cause a new row to be rendered
-      const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
+      // Add to queries, which will cause a new row to be rendered
+      const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
 
       // Ongoing transactions need to update their row indices
       const nextQueryTransactions = queryTransactions.map(qt => {
@@ -87,48 +111,38 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
 
       return {
         ...state,
-        initialQueries: nextQueries,
+        queries: nextQueries,
         logsHighlighterExpressions: undefined,
-        modifiedQueries: nextModifiedQueries,
         queryTransactions: nextQueryTransactions,
+        queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
       };
-    }
-
-    case ActionTypes.ChangeQuery: {
-      const { initialQueries, queryTransactions } = state;
-      let { modifiedQueries } = state;
-      const { query, index, override } = action.payload;
-
-      // Fast path: only change modifiedQueries to not trigger an update
-      modifiedQueries[index] = query;
-      if (!override) {
-        return {
-          ...state,
-          modifiedQueries,
-        };
-      }
+    },
+  })
+  .addMapper({
+    filter: changeQueryAction,
+    mapper: (state, action): ExploreItemState => {
+      const { queries, queryTransactions } = state;
+      const { query, index } = action.payload;
 
       // Override path: queries are completely reset
-      const nextQuery: DataQuery = {
-        ...query,
-        ...generateEmptyQuery(index),
-      };
-      const nextQueries = [...initialQueries];
+      const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
+      const nextQueries = [...queries];
       nextQueries[index] = nextQuery;
-      modifiedQueries = [...nextQueries];
 
       // Discard ongoing transaction related to row query
       const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
 
       return {
         ...state,
-        initialQueries: nextQueries,
-        modifiedQueries: nextQueries.slice(),
+        queries: nextQueries,
         queryTransactions: nextQueryTransactions,
+        queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
       };
-    }
-
-    case ActionTypes.ChangeSize: {
+    },
+  })
+  .addMapper({
+    filter: changeSizeAction,
+    mapper: (state, action): ExploreItemState => {
       const { range, datasourceInstance } = state;
       let interval = '1s';
       if (datasourceInstance && datasourceInstance.interval) {
@@ -137,67 +151,79 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       const containerWidth = action.payload.width;
       const queryIntervals = getIntervals(range, interval, containerWidth);
       return { ...state, containerWidth, queryIntervals };
-    }
-
-    case ActionTypes.ChangeTime: {
-      return {
-        ...state,
-        range: action.payload.range,
-      };
-    }
-
-    case ActionTypes.ClearQueries: {
+    },
+  })
+  .addMapper({
+    filter: changeTimeAction,
+    mapper: (state, action): ExploreItemState => {
+      return { ...state, range: action.payload.range };
+    },
+  })
+  .addMapper({
+    filter: clearQueriesAction,
+    mapper: (state): ExploreItemState => {
       const queries = ensureQueries();
       return {
         ...state,
-        initialQueries: queries.slice(),
-        modifiedQueries: queries.slice(),
+        queries: queries.slice(),
         queryTransactions: [],
         showingStartPage: Boolean(state.StartPage),
+        queryKeys: getQueryKeys(queries, state.datasourceInstance),
       };
-    }
-
-    case ActionTypes.HighlightLogsExpression: {
+    },
+  })
+  .addMapper({
+    filter: highlightLogsExpressionAction,
+    mapper: (state, action): ExploreItemState => {
       const { expressions } = action.payload;
       return { ...state, logsHighlighterExpressions: expressions };
-    }
-
-    case ActionTypes.InitializeExplore: {
-      const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
+    },
+  })
+  .addMapper({
+    filter: initializeExploreAction,
+    mapper: (state, action): ExploreItemState => {
+      const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
       return {
         ...state,
         containerWidth,
         eventBridge,
         exploreDatasources,
         range,
-        initialQueries: queries,
+        queries,
         initialized: true,
-        modifiedQueries: queries.slice(),
+        queryKeys: getQueryKeys(queries, state.datasourceInstance),
+        ...ui,
       };
-    }
-
-    case ActionTypes.UpdateDatasourceInstance: {
+    },
+  })
+  .addMapper({
+    filter: updateDatasourceInstanceAction,
+    mapper: (state, action): ExploreItemState => {
       const { datasourceInstance } = action.payload;
-      return {
-        ...state,
-        datasourceInstance,
-        datasourceName: datasourceInstance.name,
-      };
-    }
-
-    case ActionTypes.LoadDatasourceFailure: {
+      return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
+    },
+  })
+  .addMapper({
+    filter: loadDatasourceFailureAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
-    }
-
-    case ActionTypes.LoadDatasourceMissing: {
+    },
+  })
+  .addMapper({
+    filter: loadDatasourceMissingAction,
+    mapper: (state): ExploreItemState => {
       return { ...state, datasourceMissing: true, datasourceLoading: false };
-    }
-
-    case ActionTypes.LoadDatasourcePending: {
+    },
+  })
+  .addMapper({
+    filter: loadDatasourcePendingAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
-    }
-
-    case ActionTypes.LoadDatasourceSuccess: {
+    },
+  })
+  .addMapper({
+    filter: loadDatasourceSuccessAction,
+    mapper: (state, action): ExploreItemState => {
       const { containerWidth, range } = state;
       const {
         StartPage,
@@ -226,32 +252,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         logsHighlighterExpressions: undefined,
         queryTransactions: [],
       };
-    }
-
-    case ActionTypes.ModifyQueries: {
-      const { initialQueries, modifiedQueries, queryTransactions } = state;
-      const { modification, index, modifier } = action.payload as any;
+    },
+  })
+  .addMapper({
+    filter: modifyQueriesAction,
+    mapper: (state, action): ExploreItemState => {
+      const { queries, queryTransactions } = state;
+      const { modification, index, modifier } = action.payload;
       let nextQueries: DataQuery[];
       let nextQueryTransactions;
       if (index === undefined) {
         // Modify all queries
-        nextQueries = initialQueries.map((query, i) => ({
-          ...modifier(modifiedQueries[i], modification),
+        nextQueries = queries.map((query, i) => ({
+          ...modifier({ ...query }, modification),
           ...generateEmptyQuery(i),
         }));
         // Discard all ongoing transactions
         nextQueryTransactions = [];
       } else {
         // Modify query only at index
-        nextQueries = initialQueries.map((query, i) => {
+        nextQueries = queries.map((query, i) => {
           // Synchronize all queries with local query cache to ensure consistency
           // TODO still needed?
-          return i === index
-            ? {
-                ...modifier(modifiedQueries[i], modification),
-                ...generateEmptyQuery(i),
-              }
-            : query;
+          return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
         });
         nextQueryTransactions = queryTransactions
           // Consume the hint corresponding to the action
@@ -266,22 +289,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       }
       return {
         ...state,
-        initialQueries: nextQueries,
-        modifiedQueries: nextQueries.slice(),
+        queries: nextQueries,
+        queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
         queryTransactions: nextQueryTransactions,
       };
-    }
-
-    case ActionTypes.QueryTransactionFailure: {
+    },
+  })
+  .addMapper({
+    filter: queryTransactionFailureAction,
+    mapper: (state, action): ExploreItemState => {
       const { queryTransactions } = action.payload;
-      return {
-        ...state,
-        queryTransactions,
-        showingStartPage: false,
-      };
-    }
-
-    case ActionTypes.QueryTransactionStart: {
+      return { ...state, queryTransactions, showingStartPage: false };
+    },
+  })
+  .addMapper({
+    filter: queryTransactionStartAction,
+    mapper: (state, action): ExploreItemState => {
       const { queryTransactions } = state;
       const { resultType, rowIndex, transaction } = action.payload;
       // Discarding existing transactions of same type
@@ -292,14 +315,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       // Append new transaction
       const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
 
-      return {
-        ...state,
-        queryTransactions: nextQueryTransactions,
-        showingStartPage: false,
-      };
-    }
-
-    case ActionTypes.QueryTransactionSuccess: {
+      return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
+    },
+  })
+  .addMapper({
+    filter: queryTransactionSuccessAction,
+    mapper: (state, action): ExploreItemState => {
       const { datasourceInstance, queryIntervals } = state;
       const { history, queryTransactions } = action.payload;
       const results = calculateResultsFromQueryTransactions(
@@ -308,30 +329,24 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         queryIntervals.intervalMs
       );
 
-      return {
-        ...state,
-        ...results,
-        history,
-        queryTransactions,
-        showingStartPage: false,
-      };
-    }
-
-    case ActionTypes.RemoveQueryRow: {
-      const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
-      let { modifiedQueries } = state;
+      return { ...state, ...results, history, queryTransactions, showingStartPage: false };
+    },
+  })
+  .addMapper({
+    filter: removeQueryRowAction,
+    mapper: (state, action): ExploreItemState => {
+      const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
       const { index } = action.payload;
 
-      modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
-
-      if (initialQueries.length <= 1) {
+      if (queries.length <= 1) {
         return state;
       }
 
-      const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
+      const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
+      const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
 
       // Discard transactions related to row query
-      const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
+      const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
       const results = calculateResultsFromQueryTransactions(
         nextQueryTransactions,
         datasourceInstance,
@@ -341,26 +356,34 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       return {
         ...state,
         ...results,
-        initialQueries: nextQueries,
+        queries: nextQueries,
         logsHighlighterExpressions: undefined,
-        modifiedQueries: nextQueries.slice(),
         queryTransactions: nextQueryTransactions,
+        queryKeys: nextQueryKeys,
       };
-    }
-
-    case ActionTypes.RunQueriesEmpty: {
+    },
+  })
+  .addMapper({
+    filter: runQueriesEmptyAction,
+    mapper: (state): ExploreItemState => {
       return { ...state, queryTransactions: [] };
-    }
-
-    case ActionTypes.ScanRange: {
+    },
+  })
+  .addMapper({
+    filter: scanRangeAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, scanRange: action.payload.range };
-    }
-
-    case ActionTypes.ScanStart: {
+    },
+  })
+  .addMapper({
+    filter: scanStartAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, scanning: true, scanner: action.payload.scanner };
-    }
-
-    case ActionTypes.ScanStop: {
+    },
+  })
+  .addMapper({
+    filter: scanStopAction,
+    mapper: (state): ExploreItemState => {
       const { queryTransactions } = state;
       const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
       return {
@@ -370,14 +393,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         scanRange: undefined,
         scanner: undefined,
       };
-    }
-
-    case ActionTypes.SetQueries: {
+    },
+  })
+  .addMapper({
+    filter: setQueriesAction,
+    mapper: (state, action): ExploreItemState => {
       const { queries } = action.payload;
-      return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
-    }
-
-    case ActionTypes.ToggleGraph: {
+      return {
+        ...state,
+        queries: queries.slice(),
+        queryKeys: getQueryKeys(queries, state.datasourceInstance),
+      };
+    },
+  })
+  .addMapper({
+    filter: toggleGraphAction,
+    mapper: (state): ExploreItemState => {
       const showingGraph = !state.showingGraph;
       let nextQueryTransactions = state.queryTransactions;
       if (!showingGraph) {
@@ -385,9 +416,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
       }
       return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
-    }
-
-    case ActionTypes.ToggleLogs: {
+    },
+  })
+  .addMapper({
+    filter: toggleLogsAction,
+    mapper: (state): ExploreItemState => {
       const showingLogs = !state.showingLogs;
       let nextQueryTransactions = state.queryTransactions;
       if (!showingLogs) {
@@ -395,9 +428,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
       }
       return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
-    }
-
-    case ActionTypes.ToggleTable: {
+    },
+  })
+  .addMapper({
+    filter: toggleTableAction,
+    mapper: (state): ExploreItemState => {
       const showingTable = !state.showingTable;
       if (showingTable) {
         return { ...state, showingTable, queryTransactions: state.queryTransactions };
@@ -412,25 +447,26 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       );
 
       return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
-    }
-
-    case ActionTypes.QueriesImported: {
+    },
+  })
+  .addMapper({
+    filter: queriesImportedAction,
+    mapper: (state, action): ExploreItemState => {
+      const { queries } = action.payload;
       return {
         ...state,
-        initialQueries: action.payload.queries,
-        modifiedQueries: action.payload.queries.slice(),
+        queries,
+        queryKeys: getQueryKeys(queries, state.datasourceInstance),
       };
-    }
-  }
-
-  return state;
-};
+    },
+  })
+  .create();
 
 /**
  * Global Explore reducer that handles multiple Explore areas (left and right).
  * Actions that have an `exploreId` get routed to the ExploreItemReducer.
  */
-export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
+export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
   switch (action.type) {
     case ActionTypes.SplitClose: {
       return { ...state, split: false };
@@ -453,10 +489,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp
     const { exploreId } = action.payload as any;
     if (exploreId !== undefined) {
       const exploreItemState = state[exploreId];
-      return {
-        ...state,
-        [exploreId]: itemReducer(exploreItemState, action),
-      };
+      return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
     }
   }
 

+ 3 - 0
public/app/features/templating/template_srv.ts

@@ -156,6 +156,9 @@ export class TemplateSrv {
         }
         return value;
       }
+      case 'json': {
+        return JSON.stringify(value);
+      }
       case 'percentencode': {
         // like glob, but url escaped
         if (_.isArray(value)) {

+ 17 - 0
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -232,6 +232,14 @@ export default class CloudWatchDatasource {
     });
   }
 
+  getResourceARNs(region, resourceType, tags) {
+    return this.doMetricQueryRequest('resource_arns', {
+      region: this.templateSrv.replace(this.getActualRegion(region)),
+      resourceType: this.templateSrv.replace(resourceType),
+      tags: tags,
+    });
+  }
+
   metricFindQuery(query) {
     let region;
     let namespace;
@@ -293,6 +301,15 @@ export default class CloudWatchDatasource {
       return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
     }
 
+
+    const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
+    if (resourceARNsQuery) {
+      region = resourceARNsQuery[1];
+      const resourceType = resourceARNsQuery[2];
+      const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
+      return this.getResourceARNs(region, resourceType, tagsJSON);
+    }
+
     return this.$q.when([]);
   }
 

+ 23 - 0
public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts

@@ -380,6 +380,29 @@ describe('CloudWatchDatasource', () => {
     });
   });
 
+  describeMetricFindQuery('resource_arns(default,ec2:instance,{"environment":["production"]})', scenario => {
+    scenario.setup(() => {
+      scenario.requestResponse = {
+        results: {
+          metricFindQuery: {
+            tables: [{
+              rows: [[
+                'arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567',
+                'arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321'
+              ]]
+            }],
+          },
+        },
+      };
+    });
+
+    it('should call __ListMetrics and return result', () => {
+      expect(scenario.result[0].text).toContain('arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('resource_arns');
+    });
+  });
+
   it('should caclculate the correct period', () => {
     const hourSec = 60 * 60;
     const daySec = hourSec * 24;

+ 10 - 4
public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx

@@ -33,7 +33,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
       query: {
         ...this.state.query,
         expr: query.expr,
-      }
+      },
     });
   };
 
@@ -59,14 +59,20 @@ export class LokiQueryEditor extends PureComponent<Props> {
       <div>
         <LokiQueryField
           datasource={datasource}
-          initialQuery={query}
+          query={query}
           onQueryChange={this.onFieldChange}
-          onPressEnter={this.onRunQuery}
+          onExecuteQuery={this.onRunQuery}
+          history={[]}
         />
         <div className="gf-form-inline">
           <div className="gf-form">
             <div className="gf-form-label">Format as</div>
-            <Select isSearchable={false} options={formatOptions} onChange={this.onFormatChanged} value={currentFormat} />
+            <Select
+              isSearchable={false}
+              options={formatOptions}
+              onChange={this.onFormatChanged}
+              value={currentFormat}
+            />
           </div>
           <div className="gf-form gf-form--grow">
             <div className="gf-form-label gf-form-label--grow" />

+ 20 - 25
public/app/plugins/datasource/loki/components/LokiQueryField.tsx

@@ -12,12 +12,12 @@ import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explor
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
-import LokiDatasource from '../datasource';
 
 // Types
 import { LokiQuery } from '../types';
-import { TypeaheadOutput } from 'app/types/explore';
+import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
 import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise';
+import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
 
 const PRISM_SYNTAX = 'promql';
 
@@ -65,15 +65,8 @@ interface CascaderOption {
   disabled?: boolean;
 }
 
-interface LokiQueryFieldProps {
-  datasource: LokiDatasource;
-  error?: string | JSX.Element;
-  hint?: any;
-  history?: any[];
-  initialQuery?: LokiQuery;
-  onClickHintFix?: (action: any) => void;
-  onPressEnter?: () => void;
-  onQueryChange?: (value: LokiQuery, override?: boolean) => void;
+interface LokiQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> {
+  history: HistoryItem[];
 }
 
 interface LokiQueryFieldState {
@@ -98,14 +91,14 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
 
     this.plugins = [
       BracesPlugin(),
-      RunnerPlugin({ handler: props.onPressEnter }),
+      RunnerPlugin({ handler: props.onExecuteQuery }),
       PluginPrism({
         onlyIn: node => node.type === 'code_block',
         getSyntax: node => 'promql',
       }),
     ];
 
-    this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
+    this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })];
 
     this.state = {
       logLabelOptions: [],
@@ -169,20 +162,21 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
 
   onChangeQuery = (value: string, override?: boolean) => {
     // Send text change to parent
-    const { initialQuery, onQueryChange } = this.props;
+    const { query, onQueryChange, onExecuteQuery } = this.props;
     if (onQueryChange) {
-      const query = {
-        ...initialQuery,
-        expr: value,
-      };
-      onQueryChange(query, override);
+      const nextQuery = { ...query, expr: value };
+      onQueryChange(nextQuery);
+
+      if (override && onExecuteQuery) {
+        onExecuteQuery();
+      }
     }
   };
 
   onClickHintFix = () => {
-    const { hint, onClickHintFix } = this.props;
-    if (onClickHintFix && hint && hint.fix) {
-      onClickHintFix(hint.fix.action);
+    const { hint, onExecuteHint } = this.props;
+    if (onExecuteHint && hint && hint.fix) {
+      onExecuteHint(hint.fix.action);
     }
   };
 
@@ -220,7 +214,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
   };
 
   render() {
-    const { error, hint, initialQuery } = this.props;
+    const { error, hint, query } = this.props;
     const { logLabelOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
@@ -240,10 +234,11 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
             <QueryField
               additionalPlugins={this.plugins}
               cleanText={cleanText}
-              initialQuery={initialQuery.expr}
+              initialQuery={query.expr}
               onTypeahead={this.onTypeahead}
               onWillApplySuggestion={willApplySuggestion}
-              onValueChanged={this.onChangeQuery}
+              onQueryChange={this.onChangeQuery}
+              onExecuteQuery={this.props.onExecuteQuery}
               placeholder="Enter a Loki query"
               portalOrigin="loki"
               syntaxLoaded={syntaxLoaded}

+ 2 - 5
public/app/plugins/datasource/loki/components/LokiStartPage.tsx

@@ -1,11 +1,8 @@
 import React, { PureComponent } from 'react';
 import LokiCheatSheet from './LokiCheatSheet';
+import { ExploreStartPageProps } from '@grafana/ui';
 
-interface Props {
-  onClickExample: () => void;
-}
-
-export default class LokiStartPage extends PureComponent<Props> {
+export default class LokiStartPage extends PureComponent<ExploreStartPageProps> {
   render() {
     return (
       <div className="grafana-info-box grafana-info-box--max-lg">

+ 22 - 26
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -4,7 +4,7 @@ import Cascader from 'rc-cascader';
 import PluginPrism from 'slate-prism';
 import Prism from 'prismjs';
 
-import { TypeaheadOutput } from 'app/types/explore';
+import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
 
 // dom also includes Element polyfills
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
@@ -13,6 +13,7 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 import { PromQuery } from '../types';
 import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
+import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
 
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
@@ -86,15 +87,8 @@ interface CascaderOption {
   disabled?: boolean;
 }
 
-interface PromQueryFieldProps {
-  datasource: any;
-  error?: string | JSX.Element;
-  initialQuery: PromQuery;
-  hint?: any;
-  history?: any[];
-  onClickHintFix?: (action: any) => void;
-  onPressEnter?: () => void;
-  onQueryChange?: (value: PromQuery, override?: boolean) => void;
+interface PromQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, PromQuery> {
+  history: HistoryItem[];
 }
 
 interface PromQueryFieldState {
@@ -116,7 +110,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
 
     this.plugins = [
       BracesPlugin(),
-      RunnerPlugin({ handler: props.onPressEnter }),
+      RunnerPlugin({ handler: props.onExecuteQuery }),
       PluginPrism({
         onlyIn: node => node.type === 'code_block',
         getSyntax: node => 'promql',
@@ -174,20 +168,21 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
 
   onChangeQuery = (value: string, override?: boolean) => {
     // Send text change to parent
-    const { initialQuery, onQueryChange } = this.props;
+    const { query, onQueryChange, onExecuteQuery } = this.props;
     if (onQueryChange) {
-      const query: PromQuery = {
-        ...initialQuery,
-        expr: value,
-      };
-      onQueryChange(query, override);
+      const nextQuery: PromQuery = { ...query, expr: value };
+      onQueryChange(nextQuery);
+
+      if (override && onExecuteQuery) {
+        onExecuteQuery();
+      }
     }
   };
 
   onClickHintFix = () => {
-    const { hint, onClickHintFix } = this.props;
-    if (onClickHintFix && hint && hint.fix) {
-      onClickHintFix(hint.fix.action);
+    const { hint, onExecuteHint } = this.props;
+    if (onExecuteHint && hint && hint.fix) {
+      onExecuteHint(hint.fix.action);
     }
   };
 
@@ -242,29 +237,30 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
   };
 
   render() {
-    const { error, hint, initialQuery } = this.props;
+    const { error, hint, query } = this.props;
     const { metricsOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const chooserText = syntaxLoaded ? 'Metrics' : 'Loading metrics...';
 
     return (
       <>
-        <div className="gf-form-inline">
-          <div className="gf-form">
+        <div className="gf-form-inline gf-form-inline--nowrap">
+          <div className="gf-form flex-shrink-0">
             <Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
               <button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
                 {chooserText} <i className="fa fa-caret-down" />
               </button>
             </Cascader>
           </div>
-          <div className="gf-form gf-form--grow">
+          <div className="gf-form gf-form--grow flex-shrink-1">
             <QueryField
               additionalPlugins={this.plugins}
               cleanText={cleanText}
-              initialQuery={initialQuery.expr}
+              initialQuery={query.expr}
               onTypeahead={this.onTypeahead}
               onWillApplySuggestion={willApplySuggestion}
-              onValueChanged={this.onChangeQuery}
+              onQueryChange={this.onChangeQuery}
+              onExecuteQuery={this.props.onExecuteQuery}
               placeholder="Enter a PromQL query"
               portalOrigin="prometheus"
               syntaxLoaded={syntaxLoaded}

+ 2 - 5
public/app/plugins/datasource/prometheus/components/PromStart.tsx

@@ -1,11 +1,8 @@
 import React, { PureComponent } from 'react';
 import PromCheatSheet from './PromCheatSheet';
+import { ExploreStartPageProps } from '@grafana/ui';
 
-interface Props {
-  onClickExample: () => void;
-}
-
-export default class PromStart extends PureComponent<Props> {
+export default class PromStart extends PureComponent<ExploreStartPageProps> {
   render() {
     return (
       <div className="grafana-info-box grafana-info-box--max-lg">

+ 4 - 4
public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx

@@ -10,21 +10,21 @@ import { Alignments } from './Alignments';
 import { AlignmentPeriods } from './AlignmentPeriods';
 import { AliasBy } from './AliasBy';
 import { Help } from './Help';
-import { Target, MetricDescriptor } from '../types';
+import { StackdriverQuery, MetricDescriptor } from '../types';
 import { getAlignmentPickerData } from '../functions';
 import StackdriverDatasource from '../datasource';
 import { SelectOptionItem } from '@grafana/ui';
 
 export interface Props {
-  onQueryChange: (target: Target) => void;
+  onQueryChange: (target: StackdriverQuery) => void;
   onExecuteQuery: () => void;
-  target: Target;
+  target: StackdriverQuery;
   events: any;
   datasource: StackdriverDatasource;
   templateSrv: TemplateSrv;
 }
 
-interface State extends Target {
+interface State extends StackdriverQuery {
   alignOptions: SelectOptionItem[];
   lastQuery: string;
   lastQueryError: string;

+ 12 - 7
public/app/plugins/datasource/stackdriver/datasource.ts

@@ -2,9 +2,10 @@ import { stackdriverUnitMappings } from './constants';
 import appEvents from 'app/core/app_events';
 import _ from 'lodash';
 import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
-import { MetricDescriptor } from './types';
+import { StackdriverQuery, MetricDescriptor } from './types';
+import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types';
 
-export default class StackdriverDatasource {
+export default class StackdriverDatasource implements DataSourceApi<StackdriverQuery> {
   id: number;
   url: string;
   baseUrl: string;
@@ -39,9 +40,7 @@ export default class StackdriverDatasource {
           alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
           groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
           view: t.view || 'FULL',
-          filters: (t.filters || []).map(f => {
-            return this.templateSrv.replace(f, options.scopedVars || {});
-          }),
+          filters: this.interpolateFilters(t.filters, options.scopedVars),
           aliasBy: this.templateSrv.replace(t.aliasBy, options.scopedVars || {}),
           type: 'timeSeriesQuery',
         };
@@ -63,7 +62,13 @@ export default class StackdriverDatasource {
     }
   }
 
-  async getLabels(metricType, refId) {
+  interpolateFilters(filters: string[], scopedVars: object) {
+    return (filters || []).map(f => {
+      return this.templateSrv.replace(f, scopedVars || {}, 'regex');
+    });
+  }
+
+  async getLabels(metricType: string, refId: string) {
     const response = await this.getTimeSeries({
       targets: [
         {
@@ -103,7 +108,7 @@ export default class StackdriverDatasource {
     return unit;
   }
 
-  async query(options) {
+  async query(options: DataQueryOptions<StackdriverQuery>) {
     const result = [];
     const data = await this.getTimeSeries(options);
     if (data.results) {

binární
public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg


+ 2 - 2
public/app/plugins/datasource/stackdriver/plugin.json

@@ -14,8 +14,8 @@
     "description": "Google Stackdriver Datasource for Grafana",
     "version": "1.0.0",
     "logos": {
-      "small": "img/stackdriver_logo.png",
-      "large": "img/stackdriver_logo.png"
+      "small": "img/stackdriver_logo.svg",
+      "large": "img/stackdriver_logo.svg"
     },
     "author": {
       "name": "Grafana Project",

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

@@ -1,7 +1,7 @@
 import _ from 'lodash';
 
 import { QueryCtrl } from 'app/plugins/sdk';
-import { Target } from './types';
+import { StackdriverQuery } from './types';
 import { TemplateSrv } from 'app/features/templating/template_srv';
 
 export class StackdriverQueryCtrl extends QueryCtrl {
@@ -16,7 +16,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
     this.onExecuteQuery = this.onExecuteQuery.bind(this);
   }
 
-  onQueryChange(target: Target) {
+  onQueryChange(target: StackdriverQuery) {
     Object.assign(this.target, target);
   }
 

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

@@ -1,7 +1,8 @@
 import StackdriverDataSource from '../datasource';
 import { metricDescriptors } from './testData';
 import moment from 'moment';
-import { TemplateSrvStub } from 'test/specs/helpers';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+import { CustomVariable } from 'app/features/templating/all';
 
 describe('StackdriverDataSource', () => {
   const instanceSettings = {
@@ -9,7 +10,7 @@ describe('StackdriverDataSource', () => {
       defaultProject: 'testproject',
     },
   };
-  const templateSrv = new TemplateSrvStub();
+  const templateSrv = new TemplateSrv();
   const timeSrv = {};
 
   describe('when performing testDataSource', () => {
@@ -154,15 +155,41 @@ describe('StackdriverDataSource', () => {
     });
   });
 
+  describe('when interpolating a template variable for the filter', () => {
+    let interpolated;
+    describe('and is single value variable', () => {
+      beforeEach(() => {
+        const filterTemplateSrv = initTemplateSrv('filtervalue1');
+        const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
+        interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {});
+      });
+
+      it('should replace the variable with the value', () => {
+        expect(interpolated.length).toBe(3);
+        expect(interpolated[2]).toBe('filtervalue1');
+      });
+    });
+
+    describe('and is multi value variable', () => {
+      beforeEach(() => {
+        const filterTemplateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true);
+        const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
+        interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {});
+      });
+
+      it('should replace the variable with a regex expression', () => {
+        expect(interpolated[2]).toBe('(filtervalue1|filtervalue2)');
+      });
+    });
+  });
+
   describe('when interpolating a template variable for group bys', () => {
     let interpolated;
 
     describe('and is single value variable', () => {
       beforeEach(() => {
-        templateSrv.data = {
-          test: 'groupby1',
-        };
-        const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
+        const groupByTemplateSrv = initTemplateSrv('groupby1');
+        const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
         interpolated = ds.interpolateGroupBys(['[[test]]'], {});
       });
 
@@ -174,10 +201,8 @@ describe('StackdriverDataSource', () => {
 
     describe('and is multi value variable', () => {
       beforeEach(() => {
-        templateSrv.data = {
-          test: 'groupby1,groupby2',
-        };
-        const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
+        const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
+        const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
         interpolated = ds.interpolateGroupBys(['[[test]]'], {});
       });
 
@@ -241,3 +266,19 @@ describe('StackdriverDataSource', () => {
     });
   });
 });
+function initTemplateSrv(values: any, multi = false) {
+  const templateSrv = new TemplateSrv();
+  templateSrv.init([
+    new CustomVariable(
+      {
+        name: 'test',
+        current: {
+          value: values,
+        },
+        multi: multi,
+      },
+      {}
+    ),
+  ]);
+  return templateSrv;
+}

+ 12 - 8
public/app/plugins/datasource/stackdriver/types.ts

@@ -1,3 +1,5 @@
+import { DataQuery } from '@grafana/ui/src/types';
+
 export enum MetricFindQueryTypes {
   Services = 'services',
   MetricTypes = 'metricTypes',
@@ -20,20 +22,22 @@ export interface VariableQueryData {
   services: Array<{ value: string; name: string }>;
 }
 
-export interface Target {
-  defaultProject: string;
-  unit: string;
+export interface StackdriverQuery extends DataQuery {
+  defaultProject?: string;
+  unit?: string;
   metricType: string;
-  service: string;
+  service?: string;
   refId: string;
   crossSeriesReducer: string;
-  alignmentPeriod: string;
+  alignmentPeriod?: string;
   perSeriesAligner: string;
-  groupBys: string[];
-  filters: string[];
-  aliasBy: string;
+  groupBys?: string[];
+  filters?: string[];
+  aliasBy?: string;
   metricKind: string;
   valueType: string;
+  datasourceId?: number;
+  view?: string;
 }
 
 export interface AnnotationTarget {

+ 22 - 0
public/app/routes/GrafanaCtrl.ts

@@ -280,6 +280,28 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
           popover.hide();
         }
+
+        // hide time picker
+        const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0;
+        if (timePickerDropDownIsOpen) {
+          const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0;
+          const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0;
+          const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0;
+          const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0;
+
+          if (
+            targetIsInTimePickerNav ||
+            targetIsInTimePickerDropDown ||
+            targetIsDatePickerRowBtn ||
+            targetIsDatePickerHeaderBtn
+          ) {
+            return;
+          }
+
+          scope.$apply(() => {
+            scope.appEvent('closeTimepicker');
+          });
+        }
       });
     },
   };

+ 2 - 2
public/app/store/configureStore.ts

@@ -1,6 +1,6 @@
 import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
 import thunk from 'redux-thunk';
-// import { createLogger } from 'redux-logger';
+import { createLogger } from 'redux-logger';
 import sharedReducers from 'app/core/reducers';
 import alertingReducers from 'app/features/alerting/state/reducers';
 import teamsReducers from 'app/features/teams/state/reducers';
@@ -39,7 +39,7 @@ export function configureStore() {
 
   if (process.env.NODE_ENV !== 'production') {
     // DEV builds we had the logger middleware
-    setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
+    setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
   } else {
     setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
   }

+ 26 - 11
public/app/types/explore.ts

@@ -1,5 +1,14 @@
+import { ComponentClass } from 'react';
 import { Value } from 'slate';
-import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryHint } from '@grafana/ui';
+import {
+  RawTimeRange,
+  TimeRange,
+  DataQuery,
+  DataSourceSelectItem,
+  DataSourceApi,
+  QueryHint,
+  ExploreStartPageProps,
+} from '@grafana/ui';
 
 import { Emitter } from 'app/core/core';
 import { LogsModel } from 'app/core/logs_model';
@@ -102,7 +111,7 @@ export interface ExploreItemState {
   /**
    * React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet.
    */
-  StartPage?: any;
+  StartPage?: ComponentClass<ExploreStartPageProps>;
   /**
    * Width used for calculating the graph interval (can't have more datapoints than pixels)
    */
@@ -144,10 +153,10 @@ export interface ExploreItemState {
    */
   history: HistoryItem[];
   /**
-   * Initial queries for this Explore, e.g., set via URL. Each query will be
-   * converted to a query row. Query edits should be tracked in `modifiedQueries` though.
+   * Queries for this Explore, e.g., set via URL. Each query will be
+   * converted to a query row.
    */
-  initialQueries: DataQuery[];
+  queries: DataQuery[];
   /**
    * True if this Explore area has been initialized.
    * Used to distinguish URL state injection versus split view state injection.
@@ -162,12 +171,6 @@ export interface ExploreItemState {
    * Log query result to be displayed in the logs result viewer.
    */
   logsResult?: LogsModel;
-  /**
-   * Copy of `initialQueries` that tracks user edits.
-   * Don't connect this property to a react component as it is updated on every query change.
-   * Used when running queries. Needs to be reset to `initialQueries` when those are reset as well.
-   */
-  modifiedQueries: DataQuery[];
   /**
    * Query intervals for graph queries to determine how many datapoints to return.
    * Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
@@ -229,12 +232,24 @@ export interface ExploreItemState {
    * Table model that combines all query table results into a single table.
    */
   tableResult?: TableModel;
+
+  /**
+   * React keys for rendering of QueryRows
+   */
+  queryKeys: string[];
+}
+
+export interface ExploreUIState {
+  showingTable: boolean;
+  showingGraph: boolean;
+  showingLogs: boolean;
 }
 
 export interface ExploreUrlState {
   datasource: string;
   queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
   range: RawTimeRange;
+  ui: ExploreUIState;
 }
 
 export interface HistoryItem<TQuery extends DataQuery = DataQuery> {

+ 1 - 1
public/img/icons_dark_theme/icon_advanced.svg

@@ -4,7 +4,7 @@
 <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="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
 <style type="text/css">
-	.st0{fill:#0A0A0C;}
+	.st0{fill:#161719;}
 	.st1{fill:#E3E2E2;}
 </style>
 <g>

+ 1 - 1
public/img/icons_dark_theme/icon_advanced_active.svg

@@ -5,7 +5,7 @@
 	 width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
 <style type="text/css">
 	.st0{fill:url(#SVGID_1_);}
-	.st1{fill:#0A0A0C;}
+	.st1{fill:#161719;}
 	.st2{fill:url(#SVGID_2_);}
 	.st3{fill:url(#SVGID_3_);}
 	.st4{fill:url(#SVGID_4_);}

+ 1 - 1
public/img/icons_dark_theme/icon_alerting.svg

@@ -4,7 +4,7 @@
 <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="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
 <style type="text/css">
-	.st0{fill:#0A0A0C;}
+	.st0{fill:#161719;}
 	.st1{fill:#E3E2E2;}
 </style>
 <g>

+ 1 - 1
public/img/icons_dark_theme/icon_alerting_active.svg

@@ -5,7 +5,7 @@
 	 width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
 <style type="text/css">
 	.st0{fill:url(#SVGID_1_);}
-	.st1{fill:#0A0A0C;}
+	.st1{fill:#161719;}
 	.st2{fill:url(#SVGID_2_);}
 	.st3{fill:url(#SVGID_3_);}
 </style>

+ 1 - 1
public/img/icons_dark_theme/icon_query.svg

@@ -4,7 +4,7 @@
 <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="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
 <style type="text/css">
-	.st0{fill:#0A0A0C;}
+	.st0{fill:#161719;}
 	.st1{fill:#E3E2E2;}
 </style>
 <g>

+ 1 - 1
public/img/icons_dark_theme/icon_query_active.svg

@@ -5,7 +5,7 @@
 	 width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
 <style type="text/css">
 	.st0{fill:url(#SVGID_1_);}
-	.st1{fill:#0A0A0C;}
+	.st1{fill:#161719;}
 	.st2{fill:url(#SVGID_2_);}
 	.st3{fill:url(#SVGID_3_);}
 	.st4{fill:url(#SVGID_4_);}

+ 1 - 1
public/img/icons_dark_theme/icon_visualize.svg

@@ -4,7 +4,7 @@
 <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="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
 <style type="text/css">
-	.st0{fill:#0A0A0C;}
+	.st0{fill:#161719;}
 	.st1{fill:#E3E2E2;}
 </style>
 <path class="st0" d="M94.3,50C94.3,25.6,74.4,5.7,50,5.7S5.7,25.6,5.7,50S25.6,94.3,50,94.3S94.3,74.4,94.3,50z"/>

+ 1 - 1
public/img/icons_dark_theme/icon_visualize_active.svg

@@ -5,7 +5,7 @@
 	 width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
 <style type="text/css">
 	.st0{fill:url(#SVGID_1_);}
-	.st1{fill:#0A0A0C;}
+	.st1{fill:#161719;}
 	.st2{fill:url(#SVGID_2_);}
 	.st3{fill:url(#SVGID_3_);}
 </style>

+ 9 - 31
public/sass/_variables.dark.scss

@@ -66,6 +66,7 @@ $text-color-emphasis: getThemeVariable('colors.textColorEmphasis', $theme-name);
 
 $text-shadow-strong: 1px 1px 4px getThemeVariable('colors.black', $theme-name);
 $text-shadow-faint: 1px 1px 4px #2d2d2d;
+$textShadow: none;
 
 // gradients
 $brand-gradient: linear-gradient(
@@ -97,8 +98,7 @@ $hr-border-color: $dark-4;
 // Panel
 // -------------------------
 $panel-bg: #212124;
-$panel-border-color: $dark-1;
-$panel-border: solid 1px $panel-border-color;
+$panel-border: solid 1px $dark-1;
 $panel-header-hover-bg: $dark-4;
 $panel-corner: $panel-bg;
 
@@ -107,12 +107,12 @@ $page-header-bg: linear-gradient(90deg, #292a2d, black);
 $page-header-shadow: inset 0px -4px 14px $dark-2;
 $page-header-border-color: $dark-4;
 
-$divider-border-color: #555;
+$divider-border-color: $gray-1;
 
 // Graphite Target Editor
 $tight-form-bg: $dark-3;
-$tight-form-func-bg: #333334;
-$tight-form-func-highlight-bg: #444445;
+$tight-form-func-bg: $dark-4;
+$tight-form-func-highlight-bg: $dark-5;
 
 $modal-backdrop-bg: #353c42;
 $code-tag-bg: $dark-1;
@@ -134,14 +134,12 @@ $empty-list-cta-bg: $gray-blue;
 // Scrollbars
 $scrollbarBackground: #aeb5df;
 $scrollbarBackground2: #3a3a3a;
-
 $scrollbarBorder: black;
 
 // Tables
 // -------------------------
 $table-bg: transparent; // overall background-color
 $table-bg-accent: $dark-3; // for striping
-$table-bg-hover: $dark-4; // for hover
 $table-border: $dark-3; // table and cell border
 
 $table-bg-odd: $dark-2;
@@ -149,7 +147,6 @@ $table-bg-hover: $dark-3;
 
 // Buttons
 // -------------------------
-
 $btn-primary-bg: #ff6600;
 $btn-primary-bg-hl: #bc3e06;
 
@@ -170,9 +167,6 @@ $btn-inverse-bg-hl: lighten($dark-3, 4%);
 $btn-inverse-text-color: $link-color;
 $btn-inverse-text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1);
 
-$btn-active-bg: $gray-4;
-$btn-active-text-color: $blue-dark;
-
 $btn-link-color: $gray-3;
 
 $iconContainerBackground: $black;
@@ -197,6 +191,9 @@ $input-label-bg: $gray-blue;
 $input-label-border-color: $dark-3;
 $input-color-select-arrow: $white;
 
+// Input placeholder text color
+$placeholderText: darken($text-color, 25%);
+
 // Search
 $search-shadow: 0 0 30px 0 $black;
 $search-filter-box-bg: $gray-blue;
@@ -212,28 +209,19 @@ $dropdownBackground: $dark-3;
 $dropdownBorder: rgba(0, 0, 0, 0.2);
 $dropdownDividerTop: transparent;
 $dropdownDividerBottom: #444;
-$dropdownDivider: $dropdownDividerBottom;
 
 $dropdownLinkColor: $text-color;
 $dropdownLinkColorHover: $white;
 $dropdownLinkColorActive: $white;
 
-$dropdownLinkBackgroundActive: $dark-4;
 $dropdownLinkBackgroundHover: $dark-4;
 
-// COMPONENT VARIABLES
-// --------------------------------------------------
-
-// -------------------------
-$placeholderText: darken($text-color, 25%);
-
 // Horizontal forms & lists
 // -------------------------
 $horizontalComponentOffset: 180px;
 
-// Wells
+// Navbar
 // -------------------------
-
 $navbarHeight: 55px;
 
 $navbarBackground: $panel-bg;
@@ -261,9 +249,6 @@ $menu-dropdown-bg: $body-bg;
 $menu-dropdown-hover-bg: $dark-2;
 $menu-dropdown-shadow: 5px 5px 20px -5px $black;
 
-// Breadcrumb
-// -------------------------
-
 // Tabs
 // -------------------------
 $tab-border-color: $dark-4;
@@ -271,9 +256,6 @@ $tab-border-color: $dark-4;
 // Toolbar
 $toolbar-bg: $input-black;
 
-// Pagination
-// -------------------------
-
 // Form states and alerts
 // -------------------------
 $warning-text-color: $warn;
@@ -308,7 +290,6 @@ $tooltipBackground: $black;
 $tooltipColor: $gray-4;
 $tooltipArrowColor: $tooltipBackground;
 $tooltipBackgroundError: $brand-danger;
-$tooltipBackgroundBrand: $brand-primary;
 
 // images
 $checkboxImageUrl: '../img/checkbox.png';
@@ -377,9 +358,7 @@ $checkbox-color: $dark-1;
 //Panel Edit
 // -------------------------
 $panel-editor-shadow: 0 0 20px black;
-$panel-editor-border: 1px solid $dark-3;
 $panel-editor-side-menu-shadow: drop-shadow(0 0 10px $black);
-$panel-editor-toolbar-view-bg: $input-black;
 $panel-editor-viz-item-shadow: 0 0 8px $dark-5;
 $panel-editor-viz-item-border: 1px solid $dark-5;
 $panel-editor-viz-item-shadow-hover: 0 0 4px $blue;
@@ -387,7 +366,6 @@ $panel-editor-viz-item-border-hover: 1px solid $blue;
 $panel-editor-viz-item-bg: $input-black;
 $panel-editor-tabs-line-color: #e3e3e3;
 $panel-editor-viz-item-bg-hover: darken($blue, 47%);
-$panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
 
 $panel-options-group-border: none;
 $panel-options-group-header-bg: $gray-blue;

+ 41 - 74
public/sass/_variables.light.scss

@@ -1,7 +1,3 @@
-// Cosmo 2.3.2
-// Variables
-// --------------------------------------------------
-
 // Global values
 // --------------------------------------------------
 
@@ -71,12 +67,17 @@ $text-color-weak: getThemeVariable('colors.textColorWeak', $theme-name);
 $text-color-faint: getThemeVariable('colors.textColorFaint', $theme-name);
 $text-color-emphasis: getThemeVariable('colors.textColorEmphasis', $theme-name);
 
-$text-shadow-strong: none;
 $text-shadow-faint: none;
 $textShadow: none;
 
 // gradients
-$brand-gradient: linear-gradient(to right, hsl(50, 100%, 50%) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%);
+$brand-gradient: linear-gradient(
+  to right,
+  rgba(255, 213, 0, 1) 0%,
+  rgba(255, 68, 0, 1) 99%,
+  rgba(255, 68, 0, 1) 100%
+);
+
 $page-gradient: linear-gradient(180deg, $white 10px, $gray-7 100px);
 $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
 
@@ -98,10 +99,8 @@ $hr-border-color: $dark-3 !default;
 
 // Panel
 // -------------------------
-
 $panel-bg: $white;
-$panel-border-color: $gray-5;
-$panel-border: solid 1px $panel-border-color;
+$panel-border: solid 1px $gray-5;
 $panel-header-hover-bg: $gray-6;
 $panel-corner: $gray-4;
 
@@ -114,7 +113,6 @@ $divider-border-color: $gray-2;
 
 // Graphite Target Editor
 $tight-form-bg: #eaebee;
-
 $tight-form-func-bg: $gray-5;
 $tight-form-func-highlight-bg: $gray-6;
 
@@ -132,24 +130,23 @@ $list-item-bg: linear-gradient(135deg, $gray-5, $gray-6); //$card-background;
 $list-item-hover-bg: darken($gray-5, 5%);
 $list-item-link-color: $text-color;
 $list-item-shadow: $card-shadow;
+
 $empty-list-cta-bg: $gray-6;
 
+// Scrollbars
+$scrollbarBackground: $gray-5;
+$scrollbarBackground2: $gray-5;
+$scrollbarBorder: $gray-4;
+
 // Tables
 // -------------------------
 $table-bg: transparent; // overall background-color
 $table-bg-accent: $gray-5; // for striping
-$table-bg-hover: $gray-5; // for hover
-$table-bg-active: $table-bg-hover !default;
 $table-border: $gray-3; // table and cell border
 
 $table-bg-odd: $gray-6;
 $table-bg-hover: $gray-5;
 
-// Scrollbars
-$scrollbarBackground: $gray-5;
-$scrollbarBackground2: $gray-5;
-$scrollbarBorder: $gray-4;
-
 // Buttons
 // -------------------------
 $btn-primary-bg: $brand-primary;
@@ -172,16 +169,14 @@ $btn-inverse-bg-hl: darken($gray-6, 5%);
 $btn-inverse-text-color: $gray-1;
 $btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
 
-$btn-active-bg: $white;
-$btn-active-text-color: $blue;
-
 $btn-link-color: $gray-1;
 
+$iconContainerBackground: $white;
+
 $btn-divider-left: $gray-4;
 $btn-divider-right: $gray-7;
-$btn-drag-image: '../img/grab_light.svg';
 
-$iconContainerBackground: $white;
+$btn-drag-image: '../img/grab_light.svg';
 
 // Forms
 // -------------------------
@@ -198,29 +193,8 @@ $input-label-bg: $gray-5;
 $input-label-border-color: $gray-5;
 $input-color-select-arrow: $gray-1;
 
-// Sidemenu
-// -------------------------
-$side-menu-bg: $dark-2;
-$side-menu-bg-mobile: rgba(0, 0, 0, 0); //$gray-6;
-$side-menu-item-hover-bg: $gray-1;
-$side-menu-shadow: 5px 0px 10px -5px $gray-1;
-$side-menu-link-color: $gray-6;
-
-// Menu dropdowns
-// -------------------------
-$menu-dropdown-bg: $gray-7;
-$menu-dropdown-hover-bg: $gray-6;
-$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
-
-// Breadcrumb
-// -------------------------
-
-// Tabs
-// -------------------------
-$tab-border-color: $gray-5;
-
-// Toolbar
-$toolbar-bg: white;
+// Input placeholder text color
+$placeholderText: $gray-2;
 
 // search
 $search-shadow: 0 5px 30px 0 $gray-4;
@@ -237,52 +211,52 @@ $dropdownBackground: $white;
 $dropdownBorder: $gray-4;
 $dropdownDividerTop: $gray-6;
 $dropdownDividerBottom: $white;
-$dropdownDivider: $dropdownDividerTop;
 
 $dropdownLinkColor: $dark-3;
 $dropdownLinkColorHover: $link-color;
 $dropdownLinkColorActive: $link-color;
 
-$dropdownLinkBackgroundActive: $gray-6;
 $dropdownLinkBackgroundHover: $gray-6;
 
-// COMPONENT VARIABLES
-// --------------------------------------------------
-
-// Input placeholder text color
-// -------------------------
-$placeholderText: $gray-2;
-
-// Hr border color
-// -------------------------
-$hrBorder: $gray-3;
-
 // Horizontal forms & lists
 // -------------------------
 $horizontalComponentOffset: 180px;
 
-// Wells
-// -------------------------
-
 // Navbar
 // -------------------------
-
 $navbarHeight: 52px;
+
 $navbarBackground: $white;
 $navbarBorder: 1px solid $gray-4;
 $navbarShadow: 0 0 3px #c1c1c1;
 
 $navbarLinkColor: #444;
 
-$navbarBrandColor: $navbarLinkColor;
-
 $navbarButtonBackground: lighten($navbarBackground, 3%);
 $navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%);
 
 $navbar-button-border: $gray-4;
 
-// Pagination
+// Sidemenu
 // -------------------------
+$side-menu-bg: $dark-2;
+$side-menu-bg-mobile: rgba(0, 0, 0, 0); //$gray-6;
+$side-menu-item-hover-bg: $gray-1;
+$side-menu-shadow: 5px 0px 10px -5px $gray-1;
+$side-menu-link-color: $gray-6;
+
+// Menu dropdowns
+// -------------------------
+$menu-dropdown-bg: $gray-7;
+$menu-dropdown-hover-bg: $gray-6;
+$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
+
+// Tabs
+// -------------------------
+$tab-border-color: $gray-5;
+
+// Toolbar
+$toolbar-bg: white;
 
 // Form states and alerts
 // -------------------------
@@ -304,6 +278,7 @@ $popover-shadow: 0 0 20px $white;
 
 $popover-help-bg: $blue;
 $popover-help-color: $gray-6;
+
 $popover-error-bg: $btn-danger-bg;
 
 // Tooltips and popovers
@@ -317,7 +292,6 @@ $tooltipBackground: $gray-1;
 $tooltipColor: $gray-7;
 $tooltipArrowColor: $tooltipBackground; // Used by Angular tooltip
 $tooltipBackgroundError: $brand-danger;
-$tooltipBackgroundBrand: $brand-primary;
 
 // images
 $checkboxImageUrl: '../img/checkbox_white.png';
@@ -329,8 +303,6 @@ $info-box-border-color: lighten($blue, 20%);
 $footer-link-color: $gray-3;
 $footer-link-hover: $dark-5;
 
-// collapse box
-
 // json explorer
 $json-explorer-default-color: black;
 $json-explorer-string-color: green;
@@ -350,9 +322,6 @@ $json-explorer-url-color: blue;
 $diff-label-bg: $gray-5;
 $diff-label-fg: $gray-2;
 
-$diff-switch-bg: $gray-7;
-$diff-switch-disabled: $gray-5;
-
 $diff-arrow-color: $dark-3;
 $diff-group-bg: $gray-7;
 
@@ -367,6 +336,7 @@ $diff-json-new: #664e33;
 
 $diff-json-changed-fg: $gray-6;
 $diff-json-changed-num: $gray-4;
+
 $diff-json-icon: $gray-4;
 
 //Submenu
@@ -390,9 +360,7 @@ $checkbox-color: $gray-7;
 //Panel Edit
 // -------------------------
 $panel-editor-shadow: 0px 0px 8px $gray-3;
-$panel-editor-border: 1px solid $dark-4;
 $panel-editor-side-menu-shadow: drop-shadow(0 0 2px $gray-3);
-$panel-editor-toolbar-view-bg: $white;
 $panel-editor-viz-item-shadow: 0 0 4px $gray-3;
 $panel-editor-viz-item-border: 1px solid $gray-3;
 $panel-editor-viz-item-shadow-hover: 0 0 4px $blue-light;
@@ -400,7 +368,6 @@ $panel-editor-viz-item-border-hover: 1px solid $blue-light;
 $panel-editor-viz-item-bg: $white;
 $panel-editor-tabs-line-color: $dark-5;
 $panel-editor-viz-item-bg-hover: lighten($blue, 62%);
-$panel-editor-viz-item-bg-hover-active: lighten($orange, 34%);
 
 $panel-options-group-border: none;
 $panel-options-group-header-bg: $gray-5;

+ 1 - 1
public/sass/base/_icons.scss

@@ -212,7 +212,7 @@
   padding-right: 5px;
 }
 
-.panel-editor-tabs {
+.panel-editor-tabs, .add-panel-widget__icon {
   .gicon-advanced-active {
     background-image: url('../img/icons_#{$theme-name}_theme/icon_advanced_active.svg');
   }

+ 1 - 1
public/sass/components/_code_editor.scss

@@ -7,7 +7,7 @@
   &.ace_editor {
     @include font-family-monospace();
     font-size: 1rem;
-    min-height: 2.6rem;
+    min-height: 3.6rem; // Include space for horizontal scrollbar
 
     @include border-radius($input-border-radius-sm);
     border: $input-btn-border-width solid $input-border-color;

+ 4 - 0
public/sass/components/_gf-form.scss

@@ -84,6 +84,10 @@ $input-border: 1px solid $input-border-color;
   .gf-form + .gf-form {
     margin-left: $gf-form-margin;
   }
+
+  &--nowrap {
+    flex-wrap: nowrap;
+  }
 }
 
 .gf-form-button-row {

+ 0 - 1
public/sass/components/_toolbar.scss

@@ -4,7 +4,6 @@
   align-items: center;
   padding: 3px 20px 3px 20px;
   position: relative;
-  z-index: 1;
   flex: 0 0 auto;
   background: $toolbar-bg;
   border-radius: 3px;

+ 9 - 1
public/sass/utils/_utils.scss

@@ -83,10 +83,18 @@ button.close {
   position: absolute;
 }
 
-.flex-grow {
+.flex-grow-1 {
   flex-grow: 1;
 }
 
+.flex-shrink-1 {
+  flex-shrink: 1;
+}
+
+.flex-shrink-0 {
+  flex-shrink: 0;
+}
+
 .center-vh {
   display: flex;
   align-items: center;

+ 12 - 13
vendor/github.com/aws/aws-sdk-go/aws/awsutil/string_value.go

@@ -23,28 +23,27 @@ func stringValue(v reflect.Value, indent int, buf *bytes.Buffer) {
 	case reflect.Struct:
 		buf.WriteString("{\n")
 
-		names := []string{}
 		for i := 0; i < v.Type().NumField(); i++ {
-			name := v.Type().Field(i).Name
-			f := v.Field(i)
-			if name[0:1] == strings.ToLower(name[0:1]) {
+			ft := v.Type().Field(i)
+			fv := v.Field(i)
+
+			if ft.Name[0:1] == strings.ToLower(ft.Name[0:1]) {
 				continue // ignore unexported fields
 			}
-			if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice) && f.IsNil() {
+			if (fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Slice) && fv.IsNil() {
 				continue // ignore unset fields
 			}
-			names = append(names, name)
-		}
 
-		for i, n := range names {
-			val := v.FieldByName(n)
 			buf.WriteString(strings.Repeat(" ", indent+2))
-			buf.WriteString(n + ": ")
-			stringValue(val, indent+2, buf)
+			buf.WriteString(ft.Name + ": ")
 
-			if i < len(names)-1 {
-				buf.WriteString(",\n")
+			if tag := ft.Tag.Get("sensitive"); tag == "true" {
+				buf.WriteString("<sensitive>")
+			} else {
+				stringValue(fv, indent+2, buf)
 			}
+
+			buf.WriteString(",\n")
 		}
 
 		buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")

+ 1 - 1
vendor/github.com/aws/aws-sdk-go/aws/client/client.go

@@ -18,7 +18,7 @@ type Config struct {
 
 	// States that the signing name did not come from a modeled source but
 	// was derived based on other data. Used by service client constructors
-	// to determine if the signin name can be overriden based on metadata the
+	// to determine if the signin name can be overridden based on metadata the
 	// service has.
 	SigningNameDerived bool
 }

+ 52 - 8
vendor/github.com/aws/aws-sdk-go/aws/config.go

@@ -18,7 +18,7 @@ const UseServiceDefaultRetries = -1
 type RequestRetryer interface{}
 
 // A Config provides service configuration for service clients. By default,
-// all clients will use the defaults.DefaultConfig tructure.
+// all clients will use the defaults.DefaultConfig structure.
 //
 //     // Create Session with MaxRetry configuration to be shared by multiple
 //     // service clients.
@@ -45,8 +45,8 @@ type Config struct {
 	// that overrides the default generated endpoint for a client. Set this
 	// to `""` to use the default generated endpoint.
 	//
-	// @note You must still provide a `Region` value when specifying an
-	//   endpoint for a client.
+	// Note: You must still provide a `Region` value when specifying an
+	// endpoint for a client.
 	Endpoint *string
 
 	// The resolver to use for looking up endpoints for AWS service clients
@@ -65,8 +65,8 @@ type Config struct {
 	// noted. A full list of regions is found in the "Regions and Endpoints"
 	// document.
 	//
-	// @see http://docs.aws.amazon.com/general/latest/gr/rande.html
-	//   AWS Regions and Endpoints
+	// See http://docs.aws.amazon.com/general/latest/gr/rande.html for AWS
+	// Regions and Endpoints.
 	Region *string
 
 	// Set this to `true` to disable SSL when sending requests. Defaults
@@ -120,9 +120,10 @@ type Config struct {
 	// will use virtual hosted bucket addressing when possible
 	// (`http://BUCKET.s3.amazonaws.com/KEY`).
 	//
-	// @note This configuration option is specific to the Amazon S3 service.
-	// @see http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
-	//   Amazon S3: Virtual Hosting of Buckets
+	// Note: This configuration option is specific to the Amazon S3 service.
+	//
+	// See http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
+	// for Amazon S3: Virtual Hosting of Buckets
 	S3ForcePathStyle *bool
 
 	// Set this to `true` to disable the SDK adding the `Expect: 100-Continue`
@@ -223,6 +224,28 @@ type Config struct {
 	//    	Key: aws.String("//foo//bar//moo"),
 	//    })
 	DisableRestProtocolURICleaning *bool
+
+	// EnableEndpointDiscovery will allow for endpoint discovery on operations that
+	// have the definition in its model. By default, endpoint discovery is off.
+	//
+	// Example:
+	//    sess := session.Must(session.NewSession(&aws.Config{
+	//         EnableEndpointDiscovery: aws.Bool(true),
+	//    }))
+	//
+	//    svc := s3.New(sess)
+	//    out, err := svc.GetObject(&s3.GetObjectInput {
+	//    	Bucket: aws.String("bucketname"),
+	//    	Key: aws.String("/foo/bar/moo"),
+	//    })
+	EnableEndpointDiscovery *bool
+
+	// DisableEndpointHostPrefix will disable the SDK's behavior of prefixing
+	// request endpoint hosts with modeled information.
+	//
+	// Disabling this feature is useful when you want to use local endpoints
+	// for testing that do not support the modeled host prefix pattern.
+	DisableEndpointHostPrefix *bool
 }
 
 // NewConfig returns a new Config pointer that can be chained with builder
@@ -377,6 +400,19 @@ func (c *Config) WithSleepDelay(fn func(time.Duration)) *Config {
 	return c
 }
 
+// WithEndpointDiscovery will set whether or not to use endpoint discovery.
+func (c *Config) WithEndpointDiscovery(t bool) *Config {
+	c.EnableEndpointDiscovery = &t
+	return c
+}
+
+// WithDisableEndpointHostPrefix will set whether or not to use modeled host prefix
+// when making requests.
+func (c *Config) WithDisableEndpointHostPrefix(t bool) *Config {
+	c.DisableEndpointHostPrefix = &t
+	return c
+}
+
 // MergeIn merges the passed in configs into the existing config object.
 func (c *Config) MergeIn(cfgs ...*Config) {
 	for _, other := range cfgs {
@@ -476,6 +512,14 @@ func mergeInConfig(dst *Config, other *Config) {
 	if other.EnforceShouldRetryCheck != nil {
 		dst.EnforceShouldRetryCheck = other.EnforceShouldRetryCheck
 	}
+
+	if other.EnableEndpointDiscovery != nil {
+		dst.EnableEndpointDiscovery = other.EnableEndpointDiscovery
+	}
+
+	if other.DisableEndpointHostPrefix != nil {
+		dst.DisableEndpointHostPrefix = other.DisableEndpointHostPrefix
+	}
 }
 
 // Copy will return a shallow copy of the Config object. If any additional

+ 2 - 2
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/handlers.go

@@ -72,9 +72,9 @@ var ValidateReqSigHandler = request.NamedHandler{
 			signedTime = r.LastSignedAt
 		}
 
-		// 10 minutes to allow for some clock skew/delays in transmission.
+		// 5 minutes to allow for some clock skew/delays in transmission.
 		// Would be improved with aws/aws-sdk-go#423
-		if signedTime.Add(10 * time.Minute).After(time.Now()) {
+		if signedTime.Add(5 * time.Minute).After(time.Now()) {
 			return
 		}
 

+ 1 - 1
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/user_agent.go

@@ -17,7 +17,7 @@ var SDKVersionUserAgentHandler = request.NamedHandler{
 }
 
 const execEnvVar = `AWS_EXECUTION_ENV`
-const execEnvUAKey = `exec_env`
+const execEnvUAKey = `exec-env`
 
 // AddHostExecEnvUserAgentHander is a request handler appending the SDK's
 // execution environment to the user agent.

+ 1 - 3
vendor/github.com/aws/aws-sdk-go/aws/credentials/chain_provider.go

@@ -9,9 +9,7 @@ var (
 	// providers in the ChainProvider.
 	//
 	// This has been deprecated. For verbose error messaging set
-	// aws.Config.CredentialsChainVerboseErrors to true
-	//
-	// @readonly
+	// aws.Config.CredentialsChainVerboseErrors to true.
 	ErrNoValidProvidersFoundInChain = awserr.New("NoCredentialProviders",
 		`no valid providers in chain. Deprecated.
 	For verbose messaging see aws.Config.CredentialsChainVerboseErrors`,

+ 40 - 6
vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go

@@ -49,6 +49,8 @@
 package credentials
 
 import (
+	"fmt"
+	"github.com/aws/aws-sdk-go/aws/awserr"
 	"sync"
 	"time"
 )
@@ -64,8 +66,6 @@ import (
 //       Credentials: credentials.AnonymousCredentials,
 //     })))
 //     // Access public S3 buckets.
-//
-// @readonly
 var AnonymousCredentials = NewStaticCredentials("", "", "")
 
 // A Value is the AWS credentials value for individual credential fields.
@@ -99,6 +99,14 @@ type Provider interface {
 	IsExpired() bool
 }
 
+// An Expirer is an interface that Providers can implement to expose the expiration
+// time, if known.  If the Provider cannot accurately provide this info,
+// it should not implement this interface.
+type Expirer interface {
+	// The time at which the credentials are no longer valid
+	ExpiresAt() time.Time
+}
+
 // An ErrorProvider is a stub credentials provider that always returns an error
 // this is used by the SDK when construction a known provider is not possible
 // due to an error.
@@ -158,13 +166,19 @@ func (e *Expiry) SetExpiration(expiration time.Time, window time.Duration) {
 
 // IsExpired returns if the credentials are expired.
 func (e *Expiry) IsExpired() bool {
-	if e.CurrentTime == nil {
-		e.CurrentTime = time.Now
+	curTime := e.CurrentTime
+	if curTime == nil {
+		curTime = time.Now
 	}
-	return e.expiration.Before(e.CurrentTime())
+	return e.expiration.Before(curTime())
 }
 
-// A Credentials provides synchronous safe retrieval of AWS credentials Value.
+// ExpiresAt returns the expiration time of the credential
+func (e *Expiry) ExpiresAt() time.Time {
+	return e.expiration
+}
+
+// A Credentials provides concurrency safe retrieval of AWS credentials Value.
 // Credentials will cache the credentials value until they expire. Once the value
 // expires the next Get will attempt to retrieve valid credentials.
 //
@@ -256,3 +270,23 @@ func (c *Credentials) IsExpired() bool {
 func (c *Credentials) isExpired() bool {
 	return c.forceRefresh || c.provider.IsExpired()
 }
+
+// ExpiresAt provides access to the functionality of the Expirer interface of
+// the underlying Provider, if it supports that interface.  Otherwise, it returns
+// an error.
+func (c *Credentials) ExpiresAt() (time.Time, error) {
+	c.m.RLock()
+	defer c.m.RUnlock()
+
+	expirer, ok := c.provider.(Expirer)
+	if !ok {
+		return time.Time{}, awserr.New("ProviderNotExpirer",
+			fmt.Sprintf("provider %s does not support ExpiresAt()", c.creds.ProviderName),
+			nil)
+	}
+	if c.forceRefresh {
+		// set expiration time to the distant past
+		return time.Time{}, nil
+	}
+	return expirer.ExpiresAt(), nil
+}

+ 3 - 3
vendor/github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds/ec2_role_provider.go

@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"encoding/json"
 	"fmt"
-	"path"
 	"strings"
 	"time"
 
@@ -12,6 +11,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/client"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 	"github.com/aws/aws-sdk-go/aws/ec2metadata"
+	"github.com/aws/aws-sdk-go/internal/sdkuri"
 )
 
 // ProviderName provides a name of EC2Role provider
@@ -125,7 +125,7 @@ type ec2RoleCredRespBody struct {
 	Message string
 }
 
-const iamSecurityCredsPath = "/iam/security-credentials"
+const iamSecurityCredsPath = "iam/security-credentials/"
 
 // requestCredList requests a list of credentials from the EC2 service.
 // If there are no credentials, or there is an error making or receiving the request
@@ -153,7 +153,7 @@ func requestCredList(client *ec2metadata.EC2Metadata) ([]string, error) {
 // If the credentials cannot be found, or there is an error reading the response
 // and error will be returned.
 func requestCred(client *ec2metadata.EC2Metadata, credsName string) (ec2RoleCredRespBody, error) {
-	resp, err := client.GetMetadata(path.Join(iamSecurityCredsPath, credsName))
+	resp, err := client.GetMetadata(sdkuri.PathJoin(iamSecurityCredsPath, credsName))
 	if err != nil {
 		return ec2RoleCredRespBody{},
 			awserr.New("EC2RoleRequestError",

+ 7 - 0
vendor/github.com/aws/aws-sdk-go/aws/credentials/endpointcreds/provider.go

@@ -65,6 +65,10 @@ type Provider struct {
 	//
 	// If ExpiryWindow is 0 or less it will be ignored.
 	ExpiryWindow time.Duration
+
+	// Optional authorization token value if set will be used as the value of
+	// the Authorization header of the endpoint credential request.
+	AuthorizationToken string
 }
 
 // NewProviderClient returns a credentials Provider for retrieving AWS credentials
@@ -152,6 +156,9 @@ func (p *Provider) getCredentials() (*getCredentialsOutput, error) {
 	out := &getCredentialsOutput{}
 	req := p.Client.NewRequest(op, nil, out)
 	req.HTTPRequest.Header.Set("Accept", "application/json")
+	if authToken := p.AuthorizationToken; len(authToken) != 0 {
+		req.HTTPRequest.Header.Set("Authorization", authToken)
+	}
 
 	return out, req.Send()
 }

+ 0 - 4
vendor/github.com/aws/aws-sdk-go/aws/credentials/env_provider.go

@@ -12,14 +12,10 @@ const EnvProviderName = "EnvProvider"
 var (
 	// ErrAccessKeyIDNotFound is returned when the AWS Access Key ID can't be
 	// found in the process's environment.
-	//
-	// @readonly
 	ErrAccessKeyIDNotFound = awserr.New("EnvAccessKeyNotFound", "AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment", nil)
 
 	// ErrSecretAccessKeyNotFound is returned when the AWS Secret Access Key
 	// can't be found in the process's environment.
-	//
-	// @readonly
 	ErrSecretAccessKeyNotFound = awserr.New("EnvSecretNotFound", "AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment", nil)
 )
 

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů