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
* **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)

12
Gopkg.lock generated
View File

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

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:
image: grafana/loki:master
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- loki
promtail:
image: grafana/promtail:master
volumes:
- ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml
- /var/log:/var/log
- ../data/log:/var/log/grafana
command:
-config.file=/etc/promtail/docker-config.yaml
networks:
- loki

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,19 +7,13 @@ import (
// ParseIPAddress parses an IP address and removes port and/or IPV6 format
func ParseIPAddress(input string) string {
s := input
lastIndex := strings.LastIndex(input, ":")
host, _ := SplitHostPort(input)
if lastIndex != -1 {
if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
s = input[:lastIndex]
ip := net.ParseIP(host)
if ip == nil {
return host
}
}
s = strings.Replace(s, "[", "", -1)
s = strings.Replace(s, "]", "", -1)
ip := net.ParseIP(s)
if ip.IsLoopback() {
return "127.0.0.1"
@ -27,3 +21,34 @@ func ParseIPAddress(input string) 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) {
Convey("Test parse ip address", t, func() {
So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1")
})
}
func TestSplitHostPortDefault(t *testing.T) {
Convey("Test split ip address to host and port", t, func() {
host, port := SplitHostPortDefault("192.168.0.140:456", "", "")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "456")
host, port = SplitHostPortDefault("192.168.0.140", "", "123")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("[::1:456]", "", "")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "456")
host, port = SplitHostPortDefault("[::1]", "", "123")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("::1:123", "", "")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("::1", "", "123")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault(":456", "1.2.3.4", "")
So(host, ShouldEqual, "1.2.3.4")
So(port, ShouldEqual, "456")
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "123")
})
}
func TestSplitHostPort(t *testing.T) {
Convey("Test split ip address to host and port", t, func() {
host, port := SplitHostPort("192.168.0.140:456")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "456")
host, port = SplitHostPort("192.168.0.140")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "")
host, port = SplitHostPort("[::1:456]")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "456")
host, port = SplitHostPort("[::1]")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "")
host, port = SplitHostPort("::1:123")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPort("::1")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "")
host, port = SplitHostPort(":456")
So(host, ShouldEqual, "")
So(port, ShouldEqual, "456")
host, port = SplitHostPort("xyz.rds.amazonaws.com")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "")
host, port = SplitHostPort("xyz.rds.amazonaws.com:123")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "123")
})
}

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 coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export class BackendSrv {
@ -103,9 +104,16 @@ export class BackendSrv {
err => {
// handle unauthorized
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
return this.loginPing().then(() => {
return this.loginPing()
.then(() => {
options.retry = 1;
return this.request(options);
})
.catch(err => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
@ -184,12 +192,19 @@ export class BackendSrv {
// handle unauthorized for backend requests
if (requestIsLocal && firstAttempt && err.status === 401) {
return this.loginPing().then(() => {
return this.loginPing()
.then(() => {
options.retry = 1;
if (canceler) {
canceler.resolve();
}
return this.datasourceRequest(options);
})
.catch(err => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}

View File

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

View File

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

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

View File

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

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 { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,13 @@
// Types
import { Emitter } from 'app/core/core';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi } from '@grafana/ui/src/types';
import {
RawTimeRange,
TimeRange,
DataQuery,
DataSourceSelectItem,
DataSourceApi,
QueryFixAction,
} from '@grafana/ui/src/types';
import {
ExploreId,
ExploreItemState,
@ -8,234 +15,28 @@ import {
RangeScanner,
ResultType,
QueryTransaction,
ExploreUIState,
} from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
/** Higher order actions
*
*/
export enum ActionTypes {
AddQueryRow = 'explore/ADD_QUERY_ROW',
ChangeDatasource = 'explore/CHANGE_DATASOURCE',
ChangeQuery = 'explore/CHANGE_QUERY',
ChangeSize = 'explore/CHANGE_SIZE',
ChangeTime = 'explore/CHANGE_TIME',
ClearQueries = 'explore/CLEAR_QUERIES',
HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
InitializeExplore = 'explore/INITIALIZE_EXPLORE',
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
ModifyQueries = 'explore/MODIFY_QUERIES',
QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
RunQueries = 'explore/RUN_QUERIES',
RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
ScanRange = 'explore/SCAN_RANGE',
ScanStart = 'explore/SCAN_START',
ScanStop = 'explore/SCAN_STOP',
SetQueries = 'explore/SET_QUERIES',
SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN',
StateSave = 'explore/STATE_SAVE',
ToggleGraph = 'explore/TOGGLE_GRAPH',
ToggleLogs = 'explore/TOGGLE_LOGS',
ToggleTable = 'explore/TOGGLE_TABLE',
UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE',
ResetExplore = 'explore/RESET_EXPLORE',
QueriesImported = 'explore/QueriesImported',
}
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
payload: {
exploreId: ExploreId;
index: number;
query: DataQuery;
};
}
export interface 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 {
type: ActionTypes.InitializeExploreSplit;
}
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[];
};
payload: {};
}
export interface SplitCloseAction {
type: ActionTypes.SplitClose;
payload: {};
}
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 {
type: ActionTypes.ResetExplore;
payload: {};
}
export interface QueriesImported {
type: ActionTypes.QueriesImported;
payload: {
/** Lower order actions
*
*/
export interface AddQueryRowPayload {
exploreId: ExploreId;
queries: DataQuery[];
};
index: number;
query: DataQuery;
}
export type Action =
| AddQueryRowAction
| ChangeQueryAction
| ChangeSizeAction
| ChangeTimeAction
| ClearQueriesAction
| HighlightLogsExpressionAction
| InitializeExploreAction
export interface ChangeQueryPayload {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
}
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
| LoadDatasourceFailureAction
| LoadDatasourceMissingAction
| LoadDatasourcePendingAction
| LoadDatasourceSuccessAction
| ModifyQueriesAction
| QueryTransactionFailureAction
| QueryTransactionStartAction
| QueryTransactionSuccessAction
| RemoveQueryRowAction
| RunQueriesEmptyAction
| ScanRangeAction
| ScanStartAction
| ScanStopAction
| SetQueriesAction
| SplitCloseAction
| SplitOpenAction
| ToggleGraphAction
| ToggleLogsAction
| ToggleTableAction
| UpdateDatasourceInstanceAction
| ResetExploreAction
| QueriesImported;
| ActionOf<any>;
export type Action =
| ActionOf<AddQueryRowPayload>
| ActionOf<ChangeQueryPayload>
| ActionOf<ChangeSizePayload>
| ActionOf<ChangeTimePayload>
| ActionOf<ClearQueriesPayload>
| ActionOf<HighlightLogsExpressionPayload>
| ActionOf<InitializeExplorePayload>
| ActionOf<LoadDatasourceFailurePayload>
| ActionOf<LoadDatasourceMissingPayload>
| ActionOf<LoadDatasourcePendingPayload>
| ActionOf<LoadDatasourceSuccessPayload>
| ActionOf<ModifyQueriesPayload>
| ActionOf<QueryTransactionFailurePayload>
| ActionOf<QueryTransactionStartPayload>
| ActionOf<QueryTransactionSuccessPayload>
| ActionOf<RemoveQueryRowPayload>
| ActionOf<RunQueriesEmptyPayload>
| ActionOf<ScanStartPayload>
| ActionOf<ScanRangePayload>
| ActionOf<SetQueriesPayload>
| ActionOf<SplitOpenPayload>
| ActionOf<ToggleTablePayload>
| ActionOf<ToggleGraphPayload>
| ActionOf<ToggleLogsPayload>
| ActionOf<UpdateDatasourceInstancePayload>
| ActionOf<QueriesImportedPayload>;

View File

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

View File

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

View File

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

View File

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

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) {
let region;
let namespace;
@ -293,6 +301,15 @@ export default class CloudWatchDatasource {
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
}
const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
if (resourceARNsQuery) {
region = resourceARNsQuery[1];
const resourceType = resourceARNsQuery[2];
const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
return this.getResourceARNs(region, resourceType, tagsJSON);
}
return this.$q.when([]);
}

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', () => {
const hourSec = 60 * 60;
const daySec = hourSec * 24;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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",
"version": "1.0.0",
"logos": {
"small": "img/stackdriver_logo.png",
"large": "img/stackdriver_logo.png"
"small": "img/stackdriver_logo.svg",
"large": "img/stackdriver_logo.svg"
},
"author": {
"name": "Grafana Project",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
.st4{fill:url(#SVGID_4_);}

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

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">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
</style>

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

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">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
.st4{fill:url(#SVGID_4_);}

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

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">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
</style>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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