Merge branch 'master' into core/theming

This commit is contained in:
Dominik Prokop 2019-02-07 14:20:53 +01:00
commit ac50d2b31f
185 changed files with 29306 additions and 4061 deletions

View File

@ -2,6 +2,11 @@
### Minor ### Minor
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) * **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) # 6.0.0-beta1 (2019-01-30)

12
Gopkg.lock generated
View File

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

View File

@ -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

View File

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

View File

@ -74,6 +74,12 @@ Here is a minimal policy example:
"ec2:DescribeRegions" "ec2:DescribeRegions"
], ],
"Resource": "*" "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. *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`. *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`. *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). 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/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
*dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3 *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 *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 ## ec2_instance_attribute examples
@ -205,6 +214,16 @@ Example `ec2_instance_attribute()` query
ec2_instance_attribute(us-east-1, Tags.Name, { "tag:Team": [ "sysops" ] }) 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 ## Cost
Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this, Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this,

View File

@ -97,7 +97,7 @@ Creates an annotation in the Grafana database. The `dashboardId` and `panelId` f
**Example Request**: **Example Request**:
```json ```http
POST /api/annotations HTTP/1.1 POST /api/annotations HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
@ -115,7 +115,7 @@ Content-Type: application/json
**Example Response**: **Example Response**:
```json ```http
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
@ -135,7 +135,7 @@ format (string with multiple tags being separated by a space).
**Example Request**: **Example Request**:
```json ```http
POST /api/annotations/graphite HTTP/1.1 POST /api/annotations/graphite HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
@ -150,7 +150,7 @@ Content-Type: application/json
**Example Response**: **Example Response**:
```json ```http
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
@ -164,11 +164,14 @@ Content-Type: application/json
`PUT /api/annotations/:id` `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**: **Example Request**:
```json ```http
PUT /api/annotations/1141 HTTP/1.1 PUT /api/annotations/1141 HTTP/1.1
Accept: application/json Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Content-Type: application/json 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 Annotation By Id
`DELETE /api/annotations/:id` `DELETE /api/annotations/:id`
@ -201,7 +248,9 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
{"message":"Annotation deleted"} {
"message":"Annotation deleted"
}
``` ```
## Delete Annotation By RegionId ## Delete Annotation By RegionId
@ -225,5 +274,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
{"message":"Annotation region deleted"} {
"message":"Annotation region deleted"
}
``` ```

View File

@ -393,9 +393,7 @@ Analytics ID here. By default this feature is disabled.
### check_for_updates ### 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 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.
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.
<hr /> <hr />

View File

@ -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 `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 `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 `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. `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. `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. `percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.

View File

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

View File

@ -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')} />;
});

View File

@ -1,6 +1,6 @@
import { ComponentClass } from 'react'; import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel'; 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> { export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
/** /**
@ -41,6 +41,12 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
pluginExports?: PluginExports; 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> { export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
datasource: DSType; datasource: DSType;
query: TQuery; query: TQuery;
@ -48,15 +54,30 @@ export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends D
onChange: (value: TQuery) => void; 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 { export interface PluginExports {
Datasource?: DataSourceApi; Datasource?: DataSourceApi;
QueryCtrl?: any; QueryCtrl?: any;
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>; QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, DataQuery>>;
ConfigCtrl?: any; ConfigCtrl?: any;
AnnotationsQueryCtrl?: any; AnnotationsQueryCtrl?: any;
VariableQueryEditor?: any; VariableQueryEditor?: any;
ExploreQueryField?: any; ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, DataQuery>>;
ExploreStartPage?: any; ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
// Panel plugin // Panel plugin
PanelCtrl?: any; PanelCtrl?: any;
@ -114,5 +135,3 @@ export interface PluginMetaInfo {
updated: string; updated: string;
version: string; version: string;
} }

View File

@ -210,6 +210,65 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
return Success("Annotation updated") 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 { func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository() repo := annotations.GetRepository()

View File

@ -27,6 +27,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
IsRegion: false, IsRegion: false,
} }
patchCmd := dtos.PatchAnnotationsCmd{
Time: 1000,
Text: "annotation text",
Tags: []string{"tag1", "tag2"},
}
Convey("When user is an Org Viewer", func() { Convey("When user is an Org Viewer", func() {
role := m.ROLE_VIEWER role := m.ROLE_VIEWER
Convey("Should not be allowed to save an annotation", func() { 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) 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) { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@ -67,6 +78,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
So(sc.resp.Code, ShouldEqual, 200) 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) { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@ -100,6 +116,13 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
Id: 1, Id: 1,
} }
patchCmd := dtos.PatchAnnotationsCmd{
Time: 8000,
Text: "annotation text 50",
Tags: []string{"foo", "bar"},
Id: 1,
}
deleteCmd := dtos.DeleteAnnotationsCmd{ deleteCmd := dtos.DeleteAnnotationsCmd{
DashboardId: 1, DashboardId: 1,
PanelId: 1, PanelId: 1,
@ -136,6 +159,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
So(sc.resp.Code, ShouldEqual, 403) 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) { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@ -163,6 +191,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
So(sc.resp.Code, ShouldEqual, 200) 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) { loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
sc.handlerFunc = DeleteAnnotationByID sc.handlerFunc = DeleteAnnotationByID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() 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() sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200) 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) { 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() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200) 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) { func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) {
Convey(desc+" "+url, func() { Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers() defer bus.ClearBusHandlers()

View File

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

View File

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

View File

@ -22,6 +22,14 @@ type UpdateAnnotationsCmd struct {
TimeEnd int64 `json:"timeEnd"` 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 { type DeleteAnnotationsCmd struct {
AlertId int64 `json:"alertId"` AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package auth
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@ -31,7 +32,7 @@ var (
type UserAuthTokenService interface { type UserAuthTokenService interface {
InitContextWithToken(ctx *models.ReqContext, orgID int64) bool InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
UserAuthenticatedHook(user *models.User, c *models.ReqContext) error UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
UserSignedOutHook(c *models.ReqContext) SignOutUser(c *models.ReqContext) error
} }
type UserAuthTokenServiceImpl struct { 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) { func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
if setting.Env == setting.DEV { 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") ctx.Resp.Header().Del("Set-Cookie")
@ -112,8 +113,19 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m
return nil 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) s.writeSessionCookie(c, "", -1)
return err
} }
func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {

View File

@ -1,10 +1,15 @@
package auth package auth
import ( import (
"fmt"
"net/http"
"net/http/httptest"
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
@ -46,6 +51,40 @@ func TestUserAuthToken(t *testing.T) {
So(err, ShouldEqual, ErrAuthTokenNotFound) So(err, ShouldEqual, ErrAuthTokenNotFound)
So(LookupToken, ShouldBeNil) 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() { Convey("expires correctly", func() {

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2" "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/components/simplejson"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
@ -95,10 +96,11 @@ func init() {
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
"AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/ML": {"PredictCount", "PredictFailureCount"},
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"}, "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/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/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/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/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/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"}, "AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
@ -149,6 +151,7 @@ func init() {
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
"AWS/ML": {"MLModelId", "RequestMode"}, "AWS/ML": {"MLModelId", "RequestMode"},
"AWS/NATGateway": {"NatGatewayId"}, "AWS/NATGateway": {"NatGatewayId"},
"AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"},
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"}, "AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
"AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"}, "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) data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
case "ec2_instance_attribute": case "ec2_instance_attribute":
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext) data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
case "resource_arns":
data, err = e.handleGetResourceArns(ctx, parameters, queryContext)
} }
transformToTable(data, queryResult) transformToTable(data, queryResult)
@ -534,6 +539,65 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
return result, nil 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) { func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
svc, err := e.getClient(region) svc, err := e.getClient(region)
if err != nil { if err != nil {
@ -585,6 +649,28 @@ func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.
return &resp, nil 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) { func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
creds, err := GetCredentials(cwData) creds, err := GetCredentials(cwData)
if err != nil { if err != nil {

View File

@ -8,6 +8,8 @@ import (
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface" "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/bmizerany/assert"
"github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
@ -22,6 +24,11 @@ type mockedEc2 struct {
RespRegions ec2.DescribeRegionsOutput 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 { func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
fn(&m.Resp, true) fn(&m.Resp, true)
return nil return nil
@ -30,6 +37,11 @@ func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeR
return &m.RespRegions, nil 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) { func TestCloudWatchMetrics(t *testing.T) {
Convey("When calling getMetricsForCustomMetrics", t, func() { Convey("When calling getMetricsForCustomMetrics", t, func() {
@ -209,6 +221,51 @@ func TestCloudWatchMetrics(t *testing.T) {
So(result[7].Text, ShouldEqual, "vol-4-2") 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) { func TestParseMultiSelectValue(t *testing.T) {

View File

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

View File

@ -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
}

View File

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

View File

@ -9,8 +9,90 @@ import (
func TestParseIPAddress(t *testing.T) { func TestParseIPAddress(t *testing.T) {
Convey("Test parse ip address", t, func() { 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: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:456]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("[::1]"), 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")
}) })
} }

View File

@ -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")
})
}

View File

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

View File

@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null, datasource: null,
queries: [], queries: [],
range: DEFAULT_RANGE, range: DEFAULT_RANGE,
ui: {
showingGraph: true,
showingTable: true,
showingLogs: true,
}
}; };
describe('state functions', () => { describe('state functions', () => {
@ -69,9 +74,11 @@ describe('state functions', () => {
to: 'now', to: 'now',
}, },
}; };
expect(serializeStateToUrlParam(state)).toBe( expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + '{"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( 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 serialized = serializeStateToUrlParam(state);
const parsed = parseUrlState(serialized); 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); expect(state).toMatchObject(parsed);
}); });
}); });

View File

@ -11,7 +11,7 @@ import { colors } from '@grafana/ui';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
// Types // 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 TimeSeries from 'app/core/time_series2';
import { import {
ExploreUrlState, ExploreUrlState,
@ -27,6 +27,12 @@ export const DEFAULT_RANGE = {
to: 'now', to: 'now',
}; };
export const DEFAULT_UI_STATE = {
showingTable: true,
showingGraph: true,
showingLogs: true,
};
const MAX_HISTORY_ITEMS = 100; const MAX_HISTORY_ITEMS = 100;
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; 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; 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 { export function parseUrlState(initial: string | undefined): ExploreUrlState {
let uiState = DEFAULT_UI_STATE;
if (initial) { if (initial) {
try { try {
const parsed = JSON.parse(decodeURI(initial)); const parsed = JSON.parse(decodeURI(initial));
@ -160,20 +171,41 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
to: parsed[1], to: parsed[1],
}; };
const datasource = parsed[2]; const datasource = parsed[2];
const queries = parsed.slice(3); let queries = [];
return { datasource, queries, range };
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; return parsed;
} catch (e) { } catch (e) {
console.error(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 { export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
if (compact) { 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); return JSON.stringify(urlState);
} }
@ -304,3 +336,12 @@ export function clearHistory(datasourceId: string) {
const historyKey = `grafana.explore.history.${datasourceId}`; const historyKey = `grafana.explore.history.${datasourceId}`;
store.delete(historyKey); 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;
};

View File

@ -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();
});
});

View File

@ -1,12 +1,20 @@
// Libraries
import React from 'react'; import React from 'react';
import _ from 'lodash'; import _ from 'lodash';
// Utils
import config from 'app/core/config'; import config from 'app/core/config';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import store from 'app/core/store'; 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 { 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 { export interface Props {
panel: PanelModel; panel: PanelModel;
@ -46,6 +54,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
copiedPanels.push(pluginCopy); copiedPanels.push(pluginCopy);
} }
} }
return _.sortBy(copiedPanels, 'sort'); 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]); this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
} }
copyButton(panel) { onCreateNewPanel = (tab = 'queries') => {
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 = () => {
const dashboard = this.props.dashboard; const dashboard = this.props.dashboard;
const { gridPos } = this.props.panel; const { gridPos } = this.props.panel;
@ -88,7 +76,21 @@ export class AddPanelWidget extends React.Component<Props, State> {
dashboard.addPanel(newPanel); dashboard.addPanel(newPanel);
dashboard.removePanel(this.props.panel); 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 => { onPasteCopiedPanel = panelPluginInfo => {
@ -125,30 +127,50 @@ export class AddPanelWidget extends React.Component<Props, State> {
dashboard.removePanel(this.props.panel); dashboard.removePanel(this.props.panel);
}; };
render() { renderOptionLink = (icon, text, onClick) => {
let addCopyButton; 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) { render() {
addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]); const { copiedPanelPlugins } = this.state;
}
return ( return (
<div className="panel-container add-panel-widget-container"> <div className="panel-container add-panel-widget-container">
<div className="add-panel-widget"> <div className="add-panel-widget">
<div className="add-panel-widget__header grid-drag-handle"> <div className="add-panel-widget__header grid-drag-handle">
<i className="gicon gicon-add-panel" /> <i className="gicon gicon-add-panel" />
<span className="add-panel-widget__title">New Panel</span>
<button className="add-panel-widget__close" onClick={this.handleCloseAddPanel}> <button className="add-panel-widget__close" onClick={this.handleCloseAddPanel}>
<i className="fa fa-close" /> <i className="fa fa-close" />
</button> </button>
</div> </div>
<div className="add-panel-widget__btn-container"> <div className="add-panel-widget__btn-container">
<button className="btn-success btn btn-large" onClick={this.onCreateNewPanel}> <div className="add-panel-widget__create">
Edit Panel {this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)}
</button> {this.renderOptionLink('visualization', 'Choose Visualization', () =>
{addCopyButton} this.onCreateNewPanel('visualization')
<button className="btn-inverse btn" onClick={this.onCreateNewRow}> )}
Add Row </div>
</button> <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> </div>
</div> </div>

View File

@ -14,6 +14,9 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
cursor: move; cursor: move;
background: $page-header-bg;
box-shadow: $page-header-shadow;
border-bottom: 1px solid $page-header-border-color;
.gicon { .gicon {
font-size: 30px; 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 { .add-panel-widget__close {
margin-left: auto; margin-left: auto;
background-color: transparent; background-color: transparent;
@ -34,14 +60,25 @@
margin-right: -10px; 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 { .add-panel-widget__btn-container {
height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%;
flex-direction: column; flex-direction: column;
.btn {
margin-bottom: 10px;
}
} }

View File

@ -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>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,13 @@
// Types // Types
import { Emitter } from 'app/core/core'; 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 { import {
ExploreId, ExploreId,
ExploreItemState, ExploreItemState,
@ -8,234 +15,28 @@ import {
RangeScanner, RangeScanner,
ResultType, ResultType,
QueryTransaction, QueryTransaction,
ExploreUIState,
} from 'app/types/explore'; } from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
/** Higher order actions
*
*/
export enum ActionTypes { 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', 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', SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN', 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', ResetExplore = 'explore/RESET_EXPLORE',
QueriesImported = 'explore/QueriesImported',
}
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
payload: {
exploreId: ExploreId;
index: number;
query: DataQuery;
};
}
export interface ChangeQueryAction {
type: ActionTypes.ChangeQuery;
payload: {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
};
}
export interface ChangeSizeAction {
type: ActionTypes.ChangeSize;
payload: {
exploreId: ExploreId;
width: number;
height: number;
};
}
export interface ChangeTimeAction {
type: ActionTypes.ChangeTime;
payload: {
exploreId: ExploreId;
range: TimeRange;
};
}
export interface ClearQueriesAction {
type: ActionTypes.ClearQueries;
payload: {
exploreId: ExploreId;
};
}
export interface HighlightLogsExpressionAction {
type: ActionTypes.HighlightLogsExpression;
payload: {
exploreId: ExploreId;
expressions: string[];
};
}
export interface InitializeExploreAction {
type: ActionTypes.InitializeExplore;
payload: {
exploreId: ExploreId;
containerWidth: number;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
};
} }
export interface InitializeExploreSplitAction { export interface InitializeExploreSplitAction {
type: ActionTypes.InitializeExploreSplit; type: ActionTypes.InitializeExploreSplit;
} payload: {};
export interface LoadDatasourceFailureAction {
type: ActionTypes.LoadDatasourceFailure;
payload: {
exploreId: ExploreId;
error: string;
};
}
export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
payload: {
exploreId: ExploreId;
requestedDatasourceName: string;
};
}
export interface LoadDatasourceMissingAction {
type: ActionTypes.LoadDatasourceMissing;
payload: {
exploreId: ExploreId;
};
}
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 ModifyQueriesAction {
type: ActionTypes.ModifyQueries;
payload: {
exploreId: ExploreId;
modification: any;
index: number;
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
};
}
export interface QueryTransactionFailureAction {
type: ActionTypes.QueryTransactionFailure;
payload: {
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
};
}
export interface QueryTransactionStartAction {
type: ActionTypes.QueryTransactionStart;
payload: {
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
};
}
export interface QueryTransactionSuccessAction {
type: ActionTypes.QueryTransactionSuccess;
payload: {
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
};
}
export interface RemoveQueryRowAction {
type: ActionTypes.RemoveQueryRow;
payload: {
exploreId: ExploreId;
index: number;
};
}
export interface RunQueriesEmptyAction {
type: ActionTypes.RunQueriesEmpty;
payload: {
exploreId: ExploreId;
};
}
export interface ScanStartAction {
type: ActionTypes.ScanStart;
payload: {
exploreId: ExploreId;
scanner: RangeScanner;
};
}
export interface ScanRangeAction {
type: ActionTypes.ScanRange;
payload: {
exploreId: ExploreId;
range: RawTimeRange;
};
}
export interface ScanStopAction {
type: ActionTypes.ScanStop;
payload: {
exploreId: ExploreId;
};
}
export interface SetQueriesAction {
type: ActionTypes.SetQueries;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
} }
export interface SplitCloseAction { export interface SplitCloseAction {
type: ActionTypes.SplitClose; type: ActionTypes.SplitClose;
payload: {};
} }
export interface SplitOpenAction { export interface SplitOpenAction {
@ -245,80 +46,385 @@ export interface SplitOpenAction {
}; };
} }
export interface StateSaveAction {
type: ActionTypes.StateSave;
}
export interface ToggleTableAction {
type: ActionTypes.ToggleTable;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleGraphAction {
type: ActionTypes.ToggleGraph;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleLogsAction {
type: ActionTypes.ToggleLogs;
payload: {
exploreId: ExploreId;
};
}
export interface UpdateDatasourceInstanceAction {
type: ActionTypes.UpdateDatasourceInstance;
payload: {
exploreId: ExploreId;
datasourceInstance: DataSourceApi;
};
}
export interface ResetExploreAction { export interface ResetExploreAction {
type: ActionTypes.ResetExplore; type: ActionTypes.ResetExplore;
payload: {}; payload: {};
} }
export interface QueriesImported { /** Lower order actions
type: ActionTypes.QueriesImported; *
payload: { */
exploreId: ExploreId; export interface AddQueryRowPayload {
queries: DataQuery[]; exploreId: ExploreId;
}; index: number;
query: DataQuery;
} }
export type Action = export interface ChangeQueryPayload {
| AddQueryRowAction exploreId: ExploreId;
| ChangeQueryAction query: DataQuery;
| ChangeSizeAction index: number;
| ChangeTimeAction override: boolean;
| ClearQueriesAction }
| HighlightLogsExpressionAction
| InitializeExploreAction export interface ChangeSizePayload {
exploreId: ExploreId;
width: number;
height: number;
}
export interface ChangeTimePayload {
exploreId: ExploreId;
range: TimeRange;
}
export interface ClearQueriesPayload {
exploreId: ExploreId;
}
export interface HighlightLogsExpressionPayload {
exploreId: ExploreId;
expressions: string[];
}
export interface InitializeExplorePayload {
exploreId: ExploreId;
containerWidth: number;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
ui: ExploreUIState;
}
export interface LoadDatasourceFailurePayload {
exploreId: ExploreId;
error: string;
}
export interface LoadDatasourceMissingPayload {
exploreId: ExploreId;
}
export interface LoadDatasourcePendingPayload {
exploreId: ExploreId;
requestedDatasourceName: string;
}
export interface LoadDatasourceSuccessPayload {
exploreId: ExploreId;
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
logsHighlighterExpressions?: any[];
showingStartPage: boolean;
supportsGraph: boolean;
supportsLogs: boolean;
supportsTable: boolean;
}
export interface ModifyQueriesPayload {
exploreId: ExploreId;
modification: QueryFixAction;
index: number;
modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery;
}
export interface QueryTransactionFailurePayload {
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
}
export interface QueryTransactionStartPayload {
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
}
export interface QueryTransactionSuccessPayload {
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
}
export interface RemoveQueryRowPayload {
exploreId: ExploreId;
index: number;
}
export interface RunQueriesEmptyPayload {
exploreId: ExploreId;
}
export interface ScanStartPayload {
exploreId: ExploreId;
scanner: RangeScanner;
}
export interface ScanRangePayload {
exploreId: ExploreId;
range: RawTimeRange;
}
export interface ScanStopPayload {
exploreId: ExploreId;
}
export interface SetQueriesPayload {
exploreId: ExploreId;
queries: DataQuery[];
}
export interface SplitOpenPayload {
itemState: ExploreItemState;
}
export interface ToggleTablePayload {
exploreId: ExploreId;
}
export interface ToggleGraphPayload {
exploreId: ExploreId;
}
export interface ToggleLogsPayload {
exploreId: ExploreId;
}
export interface UpdateDatasourceInstancePayload {
exploreId: ExploreId;
datasourceInstance: DataSourceApi;
}
export interface QueriesImportedPayload {
exploreId: ExploreId;
queries: DataQuery[];
}
/**
* 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 | InitializeExploreSplitAction
| LoadDatasourceFailureAction
| LoadDatasourceMissingAction
| LoadDatasourcePendingAction
| LoadDatasourceSuccessAction
| ModifyQueriesAction
| QueryTransactionFailureAction
| QueryTransactionStartAction
| QueryTransactionSuccessAction
| RemoveQueryRowAction
| RunQueriesEmptyAction
| ScanRangeAction
| ScanStartAction
| ScanStopAction
| SetQueriesAction
| SplitCloseAction | SplitCloseAction
| SplitOpenAction | SplitOpenAction
| ToggleGraphAction
| ToggleLogsAction
| ToggleTableAction
| UpdateDatasourceInstanceAction
| ResetExploreAction | 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>;

View File

@ -30,40 +30,54 @@ import {
DataQuery, DataQuery,
DataSourceSelectItem, DataSourceSelectItem,
QueryHint, QueryHint,
QueryFixAction,
} from '@grafana/ui/src/types'; } from '@grafana/ui/src/types';
import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions, ExploreUIState } from 'app/types/explore';
import { import {
ExploreId, Action,
ExploreUrlState, updateDatasourceInstanceAction,
RangeScanner, changeQueryAction,
ResultType, changeSizeAction,
QueryOptions, ChangeSizePayload,
QueryTransaction, changeTimeAction,
} from 'app/types/explore'; scanStopAction,
clearQueriesAction,
import { initializeExploreAction,
Action as ThunkableAction, loadDatasourceMissingAction,
ActionTypes, loadDatasourceFailureAction,
AddQueryRowAction, loadDatasourcePendingAction,
ChangeSizeAction, queriesImportedAction,
HighlightLogsExpressionAction, LoadDatasourceSuccessPayload,
LoadDatasourceFailureAction, loadDatasourceSuccessAction,
LoadDatasourceMissingAction, modifyQueriesAction,
LoadDatasourcePendingAction, queryTransactionFailureAction,
LoadDatasourceSuccessAction, queryTransactionStartAction,
QueryTransactionStartAction, queryTransactionSuccessAction,
ScanStopAction, scanRangeAction,
UpdateDatasourceInstanceAction, runQueriesEmptyAction,
QueriesImported, scanStartAction,
setQueriesAction,
splitCloseAction,
splitOpenAction,
addQueryRowAction,
AddQueryRowPayload,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
ToggleGraphPayload,
ToggleLogsPayload,
ToggleTablePayload,
} from './actionTypes'; } 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. // * Adds a query row after the row with the given index.
*/ // */
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { export function addQueryRow(exploreId: ExploreId, index: number): ActionOf<AddQueryRowPayload> {
const query = generateEmptyQuery(index + 1); 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) => { return async (dispatch, getState) => {
const newDataSourceInstance = await getDatasourceSrv().get(datasource); const newDataSourceInstance = await getDatasourceSrv().get(datasource);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; 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(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
dispatch(loadDatasource(exploreId, 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) }; query = { ...generateEmptyQuery(index) };
} }
dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } }); dispatch(changeQueryAction({ exploreId, query, index, override }));
if (override) { if (override) {
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
} }
@ -112,8 +134,8 @@ export function changeQuery(
export function changeSize( export function changeSize(
exploreId: ExploreId, exploreId: ExploreId,
{ height, width }: { height: number; width: number } { height, width }: { height: number; width: number }
): ChangeSizeAction { ): ActionOf<ChangeSizePayload> {
return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } }; return changeSizeAction({ exploreId, height, width });
} }
/** /**
@ -121,7 +143,7 @@ export function changeSize(
*/ */
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> { export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
return dispatch => { return dispatch => {
dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } }); dispatch(changeTimeAction({ exploreId, range }));
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
}; };
} }
@ -131,19 +153,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
*/ */
export function clearQueries(exploreId: ExploreId): ThunkResult<void> { export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
return dispatch => { return dispatch => {
dispatch(scanStop(exploreId)); dispatch(scanStopAction({ exploreId }));
dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } }); dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave()); 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. * 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. * Call this only on components for with the Explore state has not been initialized.
@ -154,7 +169,8 @@ export function initializeExplore(
queries: DataQuery[], queries: DataQuery[],
range: RawTimeRange, range: RawTimeRange,
containerWidth: number, containerWidth: number,
eventBridge: Emitter eventBridge: Emitter,
ui: ExploreUIState
): ThunkResult<void> { ): ThunkResult<void> {
return async dispatch => { return async dispatch => {
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv() const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
@ -165,18 +181,17 @@ export function initializeExplore(
meta: ds.meta, meta: ds.meta,
})); }));
dispatch({ dispatch(
type: ActionTypes.InitializeExplore, initializeExploreAction({
payload: {
exploreId, exploreId,
containerWidth, containerWidth,
datasourceName,
eventBridge, eventBridge,
exploreDatasources, exploreDatasources,
queries, queries,
range, range,
}, ui,
}); })
);
if (exploreDatasources.length >= 1) { if (exploreDatasources.length >= 1) {
let instance; let instance;
@ -193,75 +208,27 @@ export function initializeExplore(
instance = await getDatasourceSrv().get(); instance = await getDatasourceSrv().get();
} }
dispatch(updateDatasourceInstance(exploreId, instance)); dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance }));
dispatch(loadDatasource(exploreId, instance));
try {
await dispatch(loadDatasource(exploreId, instance));
} catch (error) {
console.error(error);
return;
}
dispatch(runQueries(exploreId, true));
} else { } 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 * 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, * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
* e.g., Prometheus -> Loki queries. * e.g., Prometheus -> Loki queries.
*/ */
export const loadDatasourceSuccess = ( export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf<LoadDatasourceSuccessPayload> => {
exploreId: ExploreId,
instance: any,
): LoadDatasourceSuccessAction => {
// Capabilities // Capabilities
const supportsGraph = instance.meta.metrics; const supportsGraph = instance.meta.metrics;
const supportsLogs = instance.meta.logs; const supportsLogs = instance.meta.logs;
@ -274,37 +241,18 @@ export const loadDatasourceSuccess = (
// Save last-used datasource // Save last-used datasource
store.set(LAST_USED_DATASOURCE_KEY, instance.name); store.set(LAST_USED_DATASOURCE_KEY, instance.name);
return { return loadDatasourceSuccessAction({
type: ActionTypes.LoadDatasourceSuccess, exploreId,
payload: { StartPage,
exploreId, datasourceInstance: instance,
StartPage, history,
datasourceInstance: instance, showingStartPage: Boolean(StartPage),
history, supportsGraph,
showingStartPage: Boolean(StartPage), supportsLogs,
supportsGraph, supportsTable,
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( export function importQueries(
exploreId: ExploreId, exploreId: ExploreId,
queries: DataQuery[], queries: DataQuery[],
@ -326,11 +274,11 @@ export function importQueries(
} }
const nextQueries = importedQueries.map((q, i) => ({ const nextQueries = importedQueries.map((q, i) => ({
...importedQueries[i], ...q,
...generateEmptyQuery(i), ...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; const datasourceName = instance.name;
// Keep ID to track selection // Keep ID to track selection
dispatch(loadDatasourcePending(exploreId, datasourceName)); dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
let datasourceError = null; let datasourceError = null;
try { try {
const testResult = await instance.testDatasource(); const testResult = await instance.testDatasource();
datasourceError = testResult.status === 'success' ? null : testResult.message; datasourceError = testResult.status === 'success' ? null : testResult.message;
@ -353,8 +301,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
} }
if (datasourceError) { if (datasourceError) {
dispatch(loadDatasourceFailure(exploreId, datasourceError)); dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
return; return Promise.reject(`${datasourceName} loading failed`);
} }
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) { if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
@ -372,7 +320,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
} }
dispatch(loadDatasourceSuccess(exploreId, instance)); 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( export function modifyQueries(
exploreId: ExploreId, exploreId: ExploreId,
modification: any, modification: QueryFixAction,
index: number, index: number,
modifier: any modifier: any
): ThunkResult<void> { ): ThunkResult<void> {
return dispatch => { return dispatch => {
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } }); dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
if (!modification.preventSubmit) { if (!modification.preventSubmit) {
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
} }
@ -455,29 +403,10 @@ export function queryTransactionFailure(
return qt; return qt;
}); });
dispatch({ dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
type: ActionTypes.QueryTransactionFailure,
payload: { 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. * 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. * 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 // Side-effect: Saving history in localstorage
const nextHistory = updateHistory(history, datasourceId, queries); const nextHistory = updateHistory(history, datasourceId, queries);
dispatch({ dispatch(
type: ActionTypes.QueryTransactionSuccess, queryTransactionSuccessAction({
payload: {
exploreId, exploreId,
history: nextHistory, history: nextHistory,
queryTransactions: nextQueryTransactions, queryTransactions: nextQueryTransactions,
}, })
}); );
// Keep scanning for results if this was the last scanning transaction // Keep scanning for results if this was the last scanning transaction
if (scanning) { if (scanning) {
@ -549,34 +477,24 @@ export function queryTransactionSuccess(
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) { if (!other) {
const range = scanner(); const range = scanner();
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); dispatch(scanRangeAction({ exploreId, range }));
} }
} else { } else {
// We can stop scanning if we have a result // 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 * 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) => { return (dispatch, getState) => {
const { const {
datasourceInstance, datasourceInstance,
modifiedQueries, queries,
showingLogs, showingLogs,
showingGraph, showingGraph,
showingTable, showingTable,
@ -585,8 +503,8 @@ export function runQueries(exploreId: ExploreId) {
supportsTable, supportsTable,
} = getState().explore[exploreId]; } = getState().explore[exploreId];
if (!hasNonEmptyQuery(modifiedQueries)) { if (!hasNonEmptyQuery(queries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } }); dispatch(runQueriesEmptyAction({ exploreId }));
dispatch(stateSave()); // Remember to saves to state and update location dispatch(stateSave()); // Remember to saves to state and update location
return; return;
} }
@ -596,7 +514,7 @@ export function runQueries(exploreId: ExploreId) {
const interval = datasourceInstance.interval; const interval = datasourceInstance.interval;
// Keep table queries first since they need to return quickly // Keep table queries first since they need to return quickly
if (showingTable && supportsTable) { if ((ignoreUIState || showingTable) && supportsTable) {
dispatch( dispatch(
runQueriesForType( runQueriesForType(
exploreId, exploreId,
@ -611,7 +529,7 @@ export function runQueries(exploreId: ExploreId) {
) )
); );
} }
if (showingGraph && supportsGraph) { if ((ignoreUIState || showingGraph) && supportsGraph) {
dispatch( dispatch(
runQueriesForType( runQueriesForType(
exploreId, 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(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
} }
dispatch(stateSave()); dispatch(stateSave());
}; };
} }
@ -646,14 +565,7 @@ function runQueriesForType(
resultGetter?: any resultGetter?: any
) { ) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const { const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
datasourceInstance,
eventBridge,
modifiedQueries: queries,
queryIntervals,
range,
scanning,
} = getState().explore[exploreId];
const datasourceId = datasourceInstance.meta.id; const datasourceId = datasourceInstance.meta.id;
// Run all queries concurrently // Run all queries concurrently
@ -667,7 +579,7 @@ function runQueriesForType(
queryIntervals, queryIntervals,
scanning scanning
); );
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
try { try {
const now = Date.now(); const now = Date.now();
const res = await datasourceInstance.query(transaction.options); const res = await datasourceInstance.query(transaction.options);
@ -691,21 +603,14 @@ function runQueriesForType(
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> { export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
return dispatch => { return dispatch => {
// Register the scanner // Register the scanner
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } }); dispatch(scanStartAction({ exploreId, scanner }));
// Scanning must trigger query run, and return the new range // Scanning must trigger query run, and return the new range
const range = scanner(); const range = scanner();
// Set the new range to be displayed // 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. * Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run. * 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 => { return dispatch => {
// Inject react keys into query objects // Inject react keys into query objects
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
dispatch({ dispatch(setQueriesAction({ exploreId, queries }));
type: ActionTypes.SetQueries,
payload: {
exploreId,
queries,
},
});
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
}; };
} }
@ -730,7 +629,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
*/ */
export function splitClose(): ThunkResult<void> { export function splitClose(): ThunkResult<void> {
return dispatch => { return dispatch => {
dispatch({ type: ActionTypes.SplitClose }); dispatch(splitCloseAction());
dispatch(stateSave()); dispatch(stateSave());
}; };
} }
@ -747,9 +646,9 @@ export function splitOpen(): ThunkResult<void> {
const itemState = { const itemState = {
...leftState, ...leftState,
queryTransactions: [], queryTransactions: [],
initialQueries: leftState.modifiedQueries.slice(), queries: leftState.queries.slice(),
}; };
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } }); dispatch(splitOpenAction({ itemState }));
dispatch(stateSave()); dispatch(stateSave());
}; };
} }
@ -764,63 +663,74 @@ export function stateSave() {
const urlStates: { [index: string]: string } = {}; const urlStates: { [index: string]: string } = {};
const leftUrlState: ExploreUrlState = { const leftUrlState: ExploreUrlState = {
datasource: left.datasourceInstance.name, datasource: left.datasourceInstance.name,
queries: left.modifiedQueries.map(clearQueryKeys), queries: left.queries.map(clearQueryKeys),
range: left.range, range: left.range,
ui: {
showingGraph: left.showingGraph,
showingLogs: left.showingLogs,
showingTable: left.showingTable,
},
}; };
urlStates.left = serializeStateToUrlParam(leftUrlState, true); urlStates.left = serializeStateToUrlParam(leftUrlState, true);
if (split) { if (split) {
const rightUrlState: ExploreUrlState = { const rightUrlState: ExploreUrlState = {
datasource: right.datasourceInstance.name, datasource: right.datasourceInstance.name,
queries: right.modifiedQueries.map(clearQueryKeys), queries: right.queries.map(clearQueryKeys),
range: right.range, range: right.range,
ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable },
}; };
urlStates.right = serializeStateToUrlParam(rightUrlState, true); urlStates.right = serializeStateToUrlParam(rightUrlState, true);
} }
dispatch(updateLocation({ query: urlStates })); 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) => { return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); let shouldRunQueries;
if (getState().explore[exploreId].showingGraph) { 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)); dispatch(runQueries(exploreId));
} }
}; };
} };
/**
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
*/
export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
/** /**
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run. * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
*/ */
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> { export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
if (getState().explore[exploreId].showingLogs) {
dispatch(runQueries(exploreId));
}
};
}
/** /**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run. * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/ */
export function toggleTable(exploreId: ExploreId): ThunkResult<void> { export const toggleTable = togglePanelActionCreator(toggleTableAction);
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
if (getState().explore[exploreId].showingTable) {
dispatch(runQueries(exploreId));
}
};
}
/**
* Resets state for explore.
*/
export function resetExplore(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ResetExplore, payload: {} });
};
}

View File

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

View File

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

View File

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

View File

@ -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) { metricFindQuery(query) {
let region; let region;
let namespace; let namespace;
@ -293,6 +301,15 @@ export default class CloudWatchDatasource {
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson); 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([]); return this.$q.when([]);
} }

View File

@ -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', () => { it('should caclculate the correct period', () => {
const hourSec = 60 * 60; const hourSec = 60 * 60;
const daySec = hourSec * 24; const daySec = hourSec * 24;

View File

@ -33,7 +33,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
query: { query: {
...this.state.query, ...this.state.query,
expr: query.expr, expr: query.expr,
} },
}); });
}; };
@ -59,14 +59,20 @@ export class LokiQueryEditor extends PureComponent<Props> {
<div> <div>
<LokiQueryField <LokiQueryField
datasource={datasource} datasource={datasource}
initialQuery={query} query={query}
onQueryChange={this.onFieldChange} onQueryChange={this.onFieldChange}
onPressEnter={this.onRunQuery} onExecuteQuery={this.onRunQuery}
history={[]}
/> />
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <div className="gf-form">
<div className="gf-form-label">Format as</div> <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>
<div className="gf-form gf-form--grow"> <div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" /> <div className="gf-form-label gf-form-label--grow" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
<svg width="100" height="90" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M-1-1h102v92H-1z"/><g><path d="M151.637 29.785c-1.659.621-3.32 1.241-4.783 2.055-1.548-7.686-18.278-8.18-18.117 1.021.148 8.228 18.35 8.414 22.9 16.065 3.456 5.808 1.064 14.28-3.417 17.433-1.805 1.271-4.625 3.234-10.936 3.076-7.568-.19-13.65-5.277-16.065-12.305 1.474-1.151 3.464-1.777 5.468-2.393.087 9.334 18.304 12.687 20.509 3.418 3.661-15.375-24.686-9.097-24.267-25.636.375-14.998 25.388-16.197 28.708-2.734zM207.347 68.413h-5.466v-4.444c-2.872 2.517-5.263 5.222-10.254 5.467-10.316.51-17.038-10.377-10.256-17.773 4.38-4.774 13.169-5.41 20.512-2.05 1.548-10.171-13.626-11.842-16.407-4.44-1.698-.697-3.195-1.592-5.126-2.054 2.832-10.246 20.01-9.729 24.949-2.392 4.608 6.837.757 17.618 2.048 27.686zm-22.216-7.866c4.483 6.856 17.435 2.377 16.751-6.154-5.161-3.469-18.501-3.389-16.751 6.154zM416.873 53.029c-7.868.794-17.201.117-25.638.343-1.48 10.76 16.123 14.618 19.144 5.127 1.631.754 3.326 1.457 5.127 2.048-2.477 9.824-18.37 11.251-25.294 4.445-9.549-9.386-4.276-31.335 12.987-29.735 8.89.826 13.149 7.176 13.674 17.772zm-25.295-4.444h18.801c-.04-11.168-18.433-9.957-18.801 0zM347.486 36.283v32.13h-5.81v-32.13h5.81zM352.273 36.283h6.153c3.048 8.342 6.48 16.303 9.224 24.949 4.33-7.408 6.575-16.895 10.251-24.949h6.155c-4.39 10.646-8.865 21.217-12.988 32.13h-6.152c-3.907-11.019-8.635-21.217-12.643-32.13zM427.354 36.225h-5.525v32.111h5.982V48.885s1.845-9.322 11.396-7.021l2.417-5.867s-8.978-2.532-14.155 5.407l-.115-5.179zM322.434 36.225h-5.522v32.111h5.987V48.885s1.84-9.322 11.395-7.021l2.417-5.867s-8.976-2.532-14.159 5.407l-.118-5.179zM304.139 51.998c0 6.579-4.645 11.919-10.372 11.919-5.725 0-10.366-5.34-10.366-11.919 0-6.586 4.642-11.92 10.366-11.92 5.727 0 10.372 5.334 10.372 11.92zm-.107 11.916v4.19h5.742V21.038h-5.812v19.325c-2.789-3.472-6.805-5.649-11.269-5.649-8.424 0-15.253 7.768-15.253 17.341 0 9.576 6.829 17.344 15.253 17.344 4.496 0 8.536-2.21 11.33-5.724l.009.239z" fill="#6F6F6F"/><circle r="4.185" cy="25.306" cx="344.584" fill="#6F6F6F"/><path fill="#6F6F6F" d="M253.751 50.332l13.835-14.078h7.603l-12.337 12.711 13.21 19.321h-7.354l-10.346-14.959-4.738 4.861v10.225h-5.856V21.422h5.856v28.443zM236.855 46.471c-1.762-3.644-5.163-6.109-9.065-6.109-5.713 0-10.348 5.282-10.348 11.799 0 6.524 4.635 11.806 10.348 11.806 3.93 0 7.347-2.496 9.101-6.183l5.394 2.556c-2.779 5.419-8.227 9.097-14.497 9.097-9.083 0-16.451-7.733-16.451-17.275 0-9.537 7.368-17.269 16.451-17.269 6.247 0 11.683 3.653 14.467 9.041l-5.4 2.537zM160.884 26.693v9.747h-5.849v5.479h5.727v17.052s-.37 13.157 15.103 9.383l-1.947-4.995s-7.065 2.434-7.065-3.896V41.919h7.796V36.56h-7.674v-9.866h-6.091v-.001z"/><path fill="#009245" d="M50.794 41.715L27.708 84.812l46.207-.008z"/><path fill="#006837" d="M27.699 84.804L4.833 44.994h45.958z"/><path fill="#39B54A" d="M50.913 45.008H4.833L27.898 5.12H74.031z"/><path fill="#8CC63F" d="M74.031 5.12l23.236 39.84-23.352 39.844-23.002-39.796z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import StackdriverDataSource from '../datasource'; import StackdriverDataSource from '../datasource';
import { metricDescriptors } from './testData'; import { metricDescriptors } from './testData';
import moment from 'moment'; 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', () => { describe('StackdriverDataSource', () => {
const instanceSettings = { const instanceSettings = {
@ -9,7 +10,7 @@ describe('StackdriverDataSource', () => {
defaultProject: 'testproject', defaultProject: 'testproject',
}, },
}; };
const templateSrv = new TemplateSrvStub(); const templateSrv = new TemplateSrv();
const timeSrv = {}; const timeSrv = {};
describe('when performing testDataSource', () => { 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', () => { describe('when interpolating a template variable for group bys', () => {
let interpolated; let interpolated;
describe('and is single value variable', () => { describe('and is single value variable', () => {
beforeEach(() => { beforeEach(() => {
templateSrv.data = { const groupByTemplateSrv = initTemplateSrv('groupby1');
test: 'groupby1', const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {}); interpolated = ds.interpolateGroupBys(['[[test]]'], {});
}); });
@ -174,10 +201,8 @@ describe('StackdriverDataSource', () => {
describe('and is multi value variable', () => { describe('and is multi value variable', () => {
beforeEach(() => { beforeEach(() => {
templateSrv.data = { const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
test: 'groupby1,groupby2', const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {}); 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;
}

View File

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

View File

@ -280,6 +280,28 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
if (popover.length > 0 && target.parents('.graph-legend').length === 0) { if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
popover.hide(); 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');
});
}
}); });
}, },
}; };

View File

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

View File

@ -1,5 +1,14 @@
import { ComponentClass } from 'react';
import { Value } from 'slate'; 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 { Emitter } from 'app/core/core';
import { LogsModel } from 'app/core/logs_model'; 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. * 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) * Width used for calculating the graph interval (can't have more datapoints than pixels)
*/ */
@ -144,10 +153,10 @@ export interface ExploreItemState {
*/ */
history: HistoryItem[]; history: HistoryItem[];
/** /**
* Initial queries for this Explore, e.g., set via URL. Each query will be * 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. * converted to a query row.
*/ */
initialQueries: DataQuery[]; queries: DataQuery[];
/** /**
* True if this Explore area has been initialized. * True if this Explore area has been initialized.
* Used to distinguish URL state injection versus split view state injection. * 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. * Log query result to be displayed in the logs result viewer.
*/ */
logsResult?: LogsModel; 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. * Query intervals for graph queries to determine how many datapoints to return.
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed. * 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. * Table model that combines all query table results into a single table.
*/ */
tableResult?: TableModel; tableResult?: TableModel;
/**
* React keys for rendering of QueryRows
*/
queryKeys: string[];
}
export interface ExploreUIState {
showingTable: boolean;
showingGraph: boolean;
showingLogs: boolean;
} }
export interface ExploreUrlState { export interface ExploreUrlState {
datasource: string; datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
range: RawTimeRange; range: RawTimeRange;
ui: ExploreUIState;
} }
export interface HistoryItem<TQuery extends DataQuery = DataQuery> { export interface HistoryItem<TQuery extends DataQuery = DataQuery> {

View File

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

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ type Config struct {
// States that the signing name did not come from a modeled source but // States that the signing name did not come from a modeled source but
// was derived based on other data. Used by service client constructors // 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. // service has.
SigningNameDerived bool SigningNameDerived bool
} }

View File

@ -18,7 +18,7 @@ const UseServiceDefaultRetries = -1
type RequestRetryer interface{} type RequestRetryer interface{}
// A Config provides service configuration for service clients. By default, // 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 // // Create Session with MaxRetry configuration to be shared by multiple
// // service clients. // // service clients.
@ -45,8 +45,8 @@ type Config struct {
// that overrides the default generated endpoint for a client. Set this // that overrides the default generated endpoint for a client. Set this
// to `""` to use the default generated endpoint. // to `""` to use the default generated endpoint.
// //
// @note You must still provide a `Region` value when specifying an // Note: You must still provide a `Region` value when specifying an
// endpoint for a client. // endpoint for a client.
Endpoint *string Endpoint *string
// The resolver to use for looking up endpoints for AWS service clients // 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" // noted. A full list of regions is found in the "Regions and Endpoints"
// document. // document.
// //
// @see http://docs.aws.amazon.com/general/latest/gr/rande.html // See http://docs.aws.amazon.com/general/latest/gr/rande.html for AWS
// AWS Regions and Endpoints // Regions and Endpoints.
Region *string Region *string
// Set this to `true` to disable SSL when sending requests. Defaults // 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 // will use virtual hosted bucket addressing when possible
// (`http://BUCKET.s3.amazonaws.com/KEY`). // (`http://BUCKET.s3.amazonaws.com/KEY`).
// //
// @note This configuration option is specific to the Amazon S3 service. // 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 // See http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
// for Amazon S3: Virtual Hosting of Buckets
S3ForcePathStyle *bool S3ForcePathStyle *bool
// Set this to `true` to disable the SDK adding the `Expect: 100-Continue` // 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"), // Key: aws.String("//foo//bar//moo"),
// }) // })
DisableRestProtocolURICleaning *bool 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 // 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 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. // MergeIn merges the passed in configs into the existing config object.
func (c *Config) MergeIn(cfgs ...*Config) { func (c *Config) MergeIn(cfgs ...*Config) {
for _, other := range cfgs { for _, other := range cfgs {
@ -476,6 +512,14 @@ func mergeInConfig(dst *Config, other *Config) {
if other.EnforceShouldRetryCheck != nil { if other.EnforceShouldRetryCheck != nil {
dst.EnforceShouldRetryCheck = other.EnforceShouldRetryCheck 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 // Copy will return a shallow copy of the Config object. If any additional

View File

@ -72,9 +72,9 @@ var ValidateReqSigHandler = request.NamedHandler{
signedTime = r.LastSignedAt 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 // 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 return
} }

View File

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

View File

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

View File

@ -49,6 +49,8 @@
package credentials package credentials
import ( import (
"fmt"
"github.com/aws/aws-sdk-go/aws/awserr"
"sync" "sync"
"time" "time"
) )
@ -64,8 +66,6 @@ import (
// Credentials: credentials.AnonymousCredentials, // Credentials: credentials.AnonymousCredentials,
// }))) // })))
// // Access public S3 buckets. // // Access public S3 buckets.
//
// @readonly
var AnonymousCredentials = NewStaticCredentials("", "", "") var AnonymousCredentials = NewStaticCredentials("", "", "")
// A Value is the AWS credentials value for individual credential fields. // A Value is the AWS credentials value for individual credential fields.
@ -99,6 +99,14 @@ type Provider interface {
IsExpired() bool 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 // 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 // this is used by the SDK when construction a known provider is not possible
// due to an error. // 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. // IsExpired returns if the credentials are expired.
func (e *Expiry) IsExpired() bool { func (e *Expiry) IsExpired() bool {
if e.CurrentTime == nil { curTime := e.CurrentTime
e.CurrentTime = time.Now 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 // Credentials will cache the credentials value until they expire. Once the value
// expires the next Get will attempt to retrieve valid credentials. // expires the next Get will attempt to retrieve valid credentials.
// //
@ -256,3 +270,23 @@ func (c *Credentials) IsExpired() bool {
func (c *Credentials) isExpired() bool { func (c *Credentials) isExpired() bool {
return c.forceRefresh || c.provider.IsExpired() 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
}

View File

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

View File

@ -65,6 +65,10 @@ type Provider struct {
// //
// If ExpiryWindow is 0 or less it will be ignored. // If ExpiryWindow is 0 or less it will be ignored.
ExpiryWindow time.Duration 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 // NewProviderClient returns a credentials Provider for retrieving AWS credentials
@ -152,6 +156,9 @@ func (p *Provider) getCredentials() (*getCredentialsOutput, error) {
out := &getCredentialsOutput{} out := &getCredentialsOutput{}
req := p.Client.NewRequest(op, nil, out) req := p.Client.NewRequest(op, nil, out)
req.HTTPRequest.Header.Set("Accept", "application/json") req.HTTPRequest.Header.Set("Accept", "application/json")
if authToken := p.AuthorizationToken; len(authToken) != 0 {
req.HTTPRequest.Header.Set("Authorization", authToken)
}
return out, req.Send() return out, req.Send()
} }

View File

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

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