Merge branch 'master' into core/theming
@ -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
@ -37,6 +37,7 @@
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/processcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
@ -45,13 +46,18 @@
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/ini",
|
||||
"internal/s3err",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/sdkuri",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/ec2query",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/json/jsonutil",
|
||||
"private/protocol/jsonrpc",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
@ -60,11 +66,13 @@
|
||||
"service/cloudwatch",
|
||||
"service/ec2",
|
||||
"service/ec2/ec2iface",
|
||||
"service/resourcegroupstaggingapi",
|
||||
"service/resourcegroupstaggingapi/resourcegroupstaggingapiiface",
|
||||
"service/s3",
|
||||
"service/sts"
|
||||
]
|
||||
revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
|
||||
version = "v1.14.12"
|
||||
revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2"
|
||||
version = "v1.16.15"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
27
devenv/docker/blocks/loki/config.yaml
Normal 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
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
@ -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 />
|
||||
|
||||
|
@ -50,6 +50,7 @@ Filter Option | Example | Raw | Interpolated | Description
|
||||
`regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.|test2)</code> | Formats multi-value variable into a regex string
|
||||
`pipe` | ${servers:pipe} | `'test.', 'test2'` | <code>test.|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.
|
||||
|
@ -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}
|
||||
|
@ -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')} />;
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import { ComponentClass } from 'react';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
|
||||
|
||||
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
/**
|
||||
@ -41,6 +41,12 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
pluginExports?: PluginExports;
|
||||
}
|
||||
|
||||
export interface ExploreDataSourceApi<TQuery extends DataQuery = DataQuery> extends DataSourceApi {
|
||||
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
|
||||
getHighlighterExpression?(query: TQuery): string;
|
||||
languageProvider?: any;
|
||||
}
|
||||
|
||||
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||
datasource: DSType;
|
||||
query: TQuery;
|
||||
@ -48,15 +54,30 @@ export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends D
|
||||
onChange: (value: TQuery) => void;
|
||||
}
|
||||
|
||||
export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||
datasource: DSType;
|
||||
query: TQuery;
|
||||
error?: string | JSX.Element;
|
||||
hint?: QueryHint;
|
||||
history: any[];
|
||||
onExecuteQuery?: () => void;
|
||||
onQueryChange?: (value: TQuery) => void;
|
||||
onExecuteHint?: (action: QueryFixAction) => void;
|
||||
}
|
||||
|
||||
export interface ExploreStartPageProps {
|
||||
onClickExample: (query: DataQuery) => void;
|
||||
}
|
||||
|
||||
export interface PluginExports {
|
||||
Datasource?: DataSourceApi;
|
||||
QueryCtrl?: any;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, DataQuery>>;
|
||||
ConfigCtrl?: any;
|
||||
AnnotationsQueryCtrl?: any;
|
||||
VariableQueryEditor?: any;
|
||||
ExploreQueryField?: any;
|
||||
ExploreStartPage?: any;
|
||||
ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, DataQuery>>;
|
||||
ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
|
||||
|
||||
// Panel plugin
|
||||
PanelCtrl?: any;
|
||||
@ -114,5 +135,3 @@ export interface PluginMetaInfo {
|
||||
updated: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
})
|
||||
|
@ -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 }
|
||||
|
@ -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"`
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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 = "''"
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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;",
|
||||
|
@ -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
|
||||
}
|
@ -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, "", "")
|
||||
}
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
@ -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")
|
||||
})
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,86 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="panel-container add-panel-widget-container"
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget"
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__header grid-drag-handle"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-add-panel"
|
||||
/>
|
||||
<span
|
||||
className="add-panel-widget__title"
|
||||
>
|
||||
New Panel
|
||||
</span>
|
||||
<button
|
||||
className="add-panel-widget__close"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-close"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="add-panel-widget__btn-container"
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__create"
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__icon"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-queries"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
Add Query
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="add-panel-widget__icon"
|
||||
>
|
||||
<i
|
||||
className="gicon gicon-visualization"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
Choose Visualization
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="add-panel-widget__actions"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse add-panel-widget__action"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Convert to row
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,2 +1,3 @@
|
||||
export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
|
||||
export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
|
||||
export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import React, { ComponentClass } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
@ -18,34 +18,26 @@ import TableContainer from './TableContainer';
|
||||
import TimePicker, { parseTime } from './TimePicker';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
changeSize,
|
||||
changeTime,
|
||||
initializeExplore,
|
||||
modifyQueries,
|
||||
scanStart,
|
||||
scanStop,
|
||||
setQueries,
|
||||
} from './state/actions';
|
||||
import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
|
||||
|
||||
// Types
|
||||
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
|
||||
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
|
||||
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types';
|
||||
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
|
||||
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { ExploreToolbar } from './ExploreToolbar';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
|
||||
interface ExploreProps {
|
||||
StartPage?: any;
|
||||
StartPage?: ComponentClass<ExploreStartPageProps>;
|
||||
changeSize: typeof changeSize;
|
||||
changeTime: typeof changeTime;
|
||||
datasourceError: string;
|
||||
datasourceInstance: any;
|
||||
datasourceInstance: ExploreDataSourceApi;
|
||||
datasourceLoading: boolean | null;
|
||||
datasourceMissing: boolean;
|
||||
exploreId: ExploreId;
|
||||
initialQueries: DataQuery[];
|
||||
initializeExplore: typeof initializeExplore;
|
||||
initialized: boolean;
|
||||
modifyQueries: typeof modifyQueries;
|
||||
@ -54,7 +46,7 @@ interface ExploreProps {
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
scanStart: typeof scanStart;
|
||||
scanStop: typeof scanStop;
|
||||
scanStopAction: typeof scanStopAction;
|
||||
setQueries: typeof setQueries;
|
||||
split: boolean;
|
||||
showingStartPage?: boolean;
|
||||
@ -62,6 +54,7 @@ interface ExploreProps {
|
||||
supportsLogs: boolean | null;
|
||||
supportsTable: boolean | null;
|
||||
urlState: ExploreUrlState;
|
||||
queryKeys: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,18 +100,20 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
// Don't initialize on split, but need to initialize urlparameters when present
|
||||
if (!initialized) {
|
||||
// Load URL state and parse range
|
||||
const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState;
|
||||
const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
|
||||
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
||||
const initialQueries: DataQuery[] = ensureQueries(queries);
|
||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
||||
const width = this.el ? this.el.offsetWidth : 0;
|
||||
|
||||
this.props.initializeExplore(
|
||||
exploreId,
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
initialRange,
|
||||
width,
|
||||
this.exploreEvents
|
||||
this.exploreEvents,
|
||||
ui
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -171,7 +166,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
};
|
||||
|
||||
onStopScanning = () => {
|
||||
this.props.scanStop(this.props.exploreId);
|
||||
this.props.scanStopAction({ exploreId: this.props.exploreId });
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -182,12 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
datasourceLoading,
|
||||
datasourceMissing,
|
||||
exploreId,
|
||||
initialQueries,
|
||||
showingStartPage,
|
||||
split,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
queryKeys,
|
||||
} = this.props;
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
|
||||
@ -208,7 +203,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
{datasourceInstance &&
|
||||
!datasourceError && (
|
||||
<div className="explore-container">
|
||||
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} initialQueries={initialQueries} />
|
||||
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
{({ width }) => (
|
||||
<main className="m-t-2" style={{ width }}>
|
||||
@ -216,7 +211,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
||||
{!showingStartPage && (
|
||||
<>
|
||||
{supportsGraph && !supportsLogs && <GraphContainer exploreId={exploreId} />}
|
||||
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
|
||||
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
||||
{supportsLogs && (
|
||||
<LogsContainer
|
||||
@ -250,13 +245,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
datasourceInstance,
|
||||
datasourceLoading,
|
||||
datasourceMissing,
|
||||
initialQueries,
|
||||
initialized,
|
||||
range,
|
||||
showingStartPage,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
queryKeys,
|
||||
} = item;
|
||||
return {
|
||||
StartPage,
|
||||
@ -264,7 +259,6 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
datasourceInstance,
|
||||
datasourceLoading,
|
||||
datasourceMissing,
|
||||
initialQueries,
|
||||
initialized,
|
||||
range,
|
||||
showingStartPage,
|
||||
@ -272,6 +266,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
queryKeys,
|
||||
};
|
||||
}
|
||||
|
||||
@ -281,7 +276,7 @@ const mapDispatchToProps = {
|
||||
initializeExplore,
|
||||
modifyQueries,
|
||||
scanStart,
|
||||
scanStop,
|
||||
scanStopAction,
|
||||
setQueries,
|
||||
};
|
||||
|
||||
|
@ -8,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}>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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() {
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -6,25 +6,23 @@ import QueryRow from './QueryRow';
|
||||
|
||||
// Types
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
interface QueryRowsProps {
|
||||
className?: string;
|
||||
exploreEvents: Emitter;
|
||||
exploreId: ExploreId;
|
||||
initialQueries: DataQuery[];
|
||||
queryKeys: string[];
|
||||
}
|
||||
|
||||
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
||||
render() {
|
||||
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
|
||||
const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
|
||||
return (
|
||||
<div className={className}>
|
||||
{initialQueries.map((query, index) => (
|
||||
// TODO instead of relying on initialQueries, move to react key list in redux
|
||||
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
|
||||
))}
|
||||
{queryKeys.map((key, index) => {
|
||||
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,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));
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
@ -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>;
|
||||
|
@ -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,26 +250,9 @@ 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,
|
||||
queries: DataQuery[],
|
||||
@ -326,11 +274,11 @@ export function importQueries(
|
||||
}
|
||||
|
||||
const nextQueries = importedQueries.map((q, i) => ({
|
||||
...importedQueries[i],
|
||||
...q,
|
||||
...generateEmptyQuery(i),
|
||||
}));
|
||||
|
||||
dispatch(queriesImported(exploreId, nextQueries));
|
||||
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
|
||||
};
|
||||
}
|
||||
|
||||
@ -342,9 +290,9 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
||||
const datasourceName = instance.name;
|
||||
|
||||
// Keep ID to track selection
|
||||
dispatch(loadDatasourcePending(exploreId, datasourceName));
|
||||
|
||||
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
|
||||
let datasourceError = null;
|
||||
|
||||
try {
|
||||
const testResult = await instance.testDatasource();
|
||||
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||
@ -353,8 +301,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
||||
}
|
||||
|
||||
if (datasourceError) {
|
||||
dispatch(loadDatasourceFailure(exploreId, datasourceError));
|
||||
return;
|
||||
dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
|
||||
return Promise.reject(`${datasourceName} loading failed`);
|
||||
}
|
||||
|
||||
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
||||
@ -372,7 +320,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
||||
}
|
||||
|
||||
dispatch(loadDatasourceSuccess(exploreId, instance));
|
||||
dispatch(runQueries(exploreId));
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
@ -385,12 +333,12 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
||||
*/
|
||||
export function modifyQueries(
|
||||
exploreId: ExploreId,
|
||||
modification: any,
|
||||
modification: QueryFixAction,
|
||||
index: number,
|
||||
modifier: any
|
||||
): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
|
||||
dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
|
||||
if (!modification.preventSubmit) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
@ -455,29 +403,10 @@ export function queryTransactionFailure(
|
||||
return qt;
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.QueryTransactionFailure,
|
||||
payload: { exploreId, queryTransactions: nextQueryTransactions },
|
||||
});
|
||||
dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a query transaction for the given result type.
|
||||
* @param exploreId Explore area
|
||||
* @param transaction Query options and `done` status.
|
||||
* @param resultType Associate the transaction with a result viewer, e.g., Graph
|
||||
* @param rowIndex Index is used to associate latency for this transaction with a query row
|
||||
*/
|
||||
export function queryTransactionStart(
|
||||
exploreId: ExploreId,
|
||||
transaction: QueryTransaction,
|
||||
resultType: ResultType,
|
||||
rowIndex: number
|
||||
): QueryTransactionStartAction {
|
||||
return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
|
||||
* If the transaction was started by a scanner, it keeps on scanning for more results.
|
||||
@ -534,14 +463,13 @@ export function queryTransactionSuccess(
|
||||
// Side-effect: Saving history in localstorage
|
||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.QueryTransactionSuccess,
|
||||
payload: {
|
||||
dispatch(
|
||||
queryTransactionSuccessAction({
|
||||
exploreId,
|
||||
history: nextHistory,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Keep scanning for results if this was the last scanning transaction
|
||||
if (scanning) {
|
||||
@ -549,34 +477,24 @@ export function queryTransactionSuccess(
|
||||
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
|
||||
if (!other) {
|
||||
const range = scanner();
|
||||
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
|
||||
dispatch(scanRangeAction({ exploreId, range }));
|
||||
}
|
||||
} else {
|
||||
// We can stop scanning if we have a result
|
||||
dispatch(scanStop(exploreId));
|
||||
dispatch(scanStopAction({ exploreId }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove query row of the given index, as well as associated query results.
|
||||
*/
|
||||
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
||||
*/
|
||||
export function runQueries(exploreId: ExploreId) {
|
||||
export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
||||
return (dispatch, getState) => {
|
||||
const {
|
||||
datasourceInstance,
|
||||
modifiedQueries,
|
||||
queries,
|
||||
showingLogs,
|
||||
showingGraph,
|
||||
showingTable,
|
||||
@ -585,8 +503,8 @@ export function runQueries(exploreId: ExploreId) {
|
||||
supportsTable,
|
||||
} = getState().explore[exploreId];
|
||||
|
||||
if (!hasNonEmptyQuery(modifiedQueries)) {
|
||||
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
|
||||
if (!hasNonEmptyQuery(queries)) {
|
||||
dispatch(runQueriesEmptyAction({ exploreId }));
|
||||
dispatch(stateSave()); // Remember to saves to state and update location
|
||||
return;
|
||||
}
|
||||
@ -596,7 +514,7 @@ export function runQueries(exploreId: ExploreId) {
|
||||
const interval = datasourceInstance.interval;
|
||||
|
||||
// Keep table queries first since they need to return quickly
|
||||
if (showingTable && supportsTable) {
|
||||
if ((ignoreUIState || showingTable) && supportsTable) {
|
||||
dispatch(
|
||||
runQueriesForType(
|
||||
exploreId,
|
||||
@ -611,7 +529,7 @@ export function runQueries(exploreId: ExploreId) {
|
||||
)
|
||||
);
|
||||
}
|
||||
if (showingGraph && supportsGraph) {
|
||||
if ((ignoreUIState || showingGraph) && supportsGraph) {
|
||||
dispatch(
|
||||
runQueriesForType(
|
||||
exploreId,
|
||||
@ -625,9 +543,10 @@ export function runQueries(exploreId: ExploreId) {
|
||||
)
|
||||
);
|
||||
}
|
||||
if (showingLogs && supportsLogs) {
|
||||
if ((ignoreUIState || showingLogs) && supportsLogs) {
|
||||
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
|
||||
}
|
||||
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
@ -646,14 +565,7 @@ function runQueriesForType(
|
||||
resultGetter?: any
|
||||
) {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
datasourceInstance,
|
||||
eventBridge,
|
||||
modifiedQueries: queries,
|
||||
queryIntervals,
|
||||
range,
|
||||
scanning,
|
||||
} = getState().explore[exploreId];
|
||||
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
|
||||
const datasourceId = datasourceInstance.meta.id;
|
||||
|
||||
// Run all queries concurrently
|
||||
@ -667,7 +579,7 @@ function runQueriesForType(
|
||||
queryIntervals,
|
||||
scanning
|
||||
);
|
||||
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
|
||||
dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
|
||||
try {
|
||||
const now = Date.now();
|
||||
const res = await datasourceInstance.query(transaction.options);
|
||||
@ -691,21 +603,14 @@ function runQueriesForType(
|
||||
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
// Register the scanner
|
||||
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
|
||||
dispatch(scanStartAction({ exploreId, scanner }));
|
||||
// Scanning must trigger query run, and return the new range
|
||||
const range = scanner();
|
||||
// Set the new range to be displayed
|
||||
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
|
||||
dispatch(scanRangeAction({ exploreId, range }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any scanning for more results.
|
||||
*/
|
||||
export function scanStop(exploreId: ExploreId): ScanStopAction {
|
||||
return { type: ActionTypes.ScanStop, payload: { exploreId } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset queries to the given queries. Any modifications will be discarded.
|
||||
* Use this action for clicks on query examples. Triggers a query run.
|
||||
@ -714,13 +619,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
|
||||
return dispatch => {
|
||||
// Inject react keys into query objects
|
||||
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
|
||||
dispatch({
|
||||
type: ActionTypes.SetQueries,
|
||||
payload: {
|
||||
exploreId,
|
||||
queries,
|
||||
},
|
||||
});
|
||||
dispatch(setQueriesAction({ exploreId, queries }));
|
||||
dispatch(runQueries(exploreId));
|
||||
};
|
||||
}
|
||||
@ -730,7 +629,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
|
||||
*/
|
||||
export function splitClose(): ThunkResult<void> {
|
||||
return dispatch => {
|
||||
dispatch({ type: ActionTypes.SplitClose });
|
||||
dispatch(splitCloseAction());
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
@ -747,9 +646,9 @@ export function splitOpen(): ThunkResult<void> {
|
||||
const itemState = {
|
||||
...leftState,
|
||||
queryTransactions: [],
|
||||
initialQueries: leftState.modifiedQueries.slice(),
|
||||
queries: leftState.queries.slice(),
|
||||
};
|
||||
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
|
||||
dispatch(splitOpenAction({ itemState }));
|
||||
dispatch(stateSave());
|
||||
};
|
||||
}
|
||||
@ -764,63 +663,74 @@ export function stateSave() {
|
||||
const urlStates: { [index: string]: string } = {};
|
||||
const leftUrlState: ExploreUrlState = {
|
||||
datasource: left.datasourceInstance.name,
|
||||
queries: left.modifiedQueries.map(clearQueryKeys),
|
||||
queries: left.queries.map(clearQueryKeys),
|
||||
range: left.range,
|
||||
ui: {
|
||||
showingGraph: left.showingGraph,
|
||||
showingLogs: left.showingLogs,
|
||||
showingTable: left.showingTable,
|
||||
},
|
||||
};
|
||||
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
||||
if (split) {
|
||||
const rightUrlState: ExploreUrlState = {
|
||||
datasource: right.datasourceInstance.name,
|
||||
queries: right.modifiedQueries.map(clearQueryKeys),
|
||||
queries: right.queries.map(clearQueryKeys),
|
||||
range: right.range,
|
||||
ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable },
|
||||
};
|
||||
|
||||
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
||||
}
|
||||
|
||||
dispatch(updateLocation({ query: urlStates }));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
||||
* Creates action to collapse graph/logs/table panel. When panel is collapsed,
|
||||
* queries won't be run
|
||||
*/
|
||||
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
|
||||
const togglePanelActionCreator = (
|
||||
actionCreator:
|
||||
| ActionCreator<ToggleGraphPayload>
|
||||
| ActionCreator<ToggleLogsPayload>
|
||||
| ActionCreator<ToggleTablePayload>
|
||||
) => (exploreId: ExploreId) => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
|
||||
if (getState().explore[exploreId].showingGraph) {
|
||||
let shouldRunQueries;
|
||||
dispatch(actionCreator({ exploreId }));
|
||||
dispatch(stateSave());
|
||||
|
||||
switch (actionCreator.type) {
|
||||
case toggleGraphAction.type:
|
||||
shouldRunQueries = getState().explore[exploreId].showingGraph;
|
||||
break;
|
||||
case toggleLogsAction.type:
|
||||
shouldRunQueries = getState().explore[exploreId].showingLogs;
|
||||
break;
|
||||
case toggleTableAction.type:
|
||||
shouldRunQueries = getState().explore[exploreId].showingTable;
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldRunQueries) {
|
||||
dispatch(runQueries(exploreId));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand/collapse the 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);
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,11 +3,41 @@ import {
|
||||
generateEmptyQuery,
|
||||
getIntervals,
|
||||
ensureQueries,
|
||||
getQueryKeys,
|
||||
} from 'app/core/utils/explore';
|
||||
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
import { Action, ActionTypes } from './actionTypes';
|
||||
import { HigherOrderAction, ActionTypes } from './actionTypes';
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import {
|
||||
addQueryRowAction,
|
||||
changeQueryAction,
|
||||
changeSizeAction,
|
||||
changeTimeAction,
|
||||
clearQueriesAction,
|
||||
highlightLogsExpressionAction,
|
||||
initializeExploreAction,
|
||||
updateDatasourceInstanceAction,
|
||||
loadDatasourceFailureAction,
|
||||
loadDatasourceMissingAction,
|
||||
loadDatasourcePendingAction,
|
||||
loadDatasourceSuccessAction,
|
||||
modifyQueriesAction,
|
||||
queryTransactionFailureAction,
|
||||
queryTransactionStartAction,
|
||||
queryTransactionSuccessAction,
|
||||
removeQueryRowAction,
|
||||
runQueriesEmptyAction,
|
||||
scanRangeAction,
|
||||
scanStartAction,
|
||||
scanStopAction,
|
||||
setQueriesAction,
|
||||
toggleGraphAction,
|
||||
toggleLogsAction,
|
||||
toggleTableAction,
|
||||
queriesImportedAction,
|
||||
} from './actionTypes';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
@ -30,9 +60,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
||||
datasourceMissing: false,
|
||||
exploreDatasources: [],
|
||||
history: [],
|
||||
initialQueries: [],
|
||||
queries: [],
|
||||
initialized: false,
|
||||
modifiedQueries: [],
|
||||
queryTransactions: [],
|
||||
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
|
||||
range: DEFAULT_RANGE,
|
||||
@ -44,6 +73,7 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
queryKeys: [],
|
||||
});
|
||||
|
||||
/**
|
||||
@ -58,21 +88,15 @@ export const initialExploreState: ExploreState = {
|
||||
/**
|
||||
* Reducer for an Explore area, to be used by the global Explore reducer.
|
||||
*/
|
||||
export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.AddQueryRow: {
|
||||
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||
export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
|
||||
.addMapper({
|
||||
filter: addQueryRowAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queries, queryTransactions } = state;
|
||||
const { index, query } = action.payload;
|
||||
|
||||
// Add new query row after given index, keep modifications of existing rows
|
||||
const nextModifiedQueries = [
|
||||
...modifiedQueries.slice(0, index + 1),
|
||||
{ ...query },
|
||||
...initialQueries.slice(index + 1),
|
||||
];
|
||||
|
||||
// Add to initialQueries, which will cause a new row to be rendered
|
||||
const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
|
||||
// Add to queries, which will cause a new row to be rendered
|
||||
const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
|
||||
|
||||
// Ongoing transactions need to update their row indices
|
||||
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||
@ -87,48 +111,38 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
|
||||
return {
|
||||
...state,
|
||||
initialQueries: nextQueries,
|
||||
queries: nextQueries,
|
||||
logsHighlighterExpressions: undefined,
|
||||
modifiedQueries: nextModifiedQueries,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ChangeQuery: {
|
||||
const { initialQueries, queryTransactions } = state;
|
||||
let { modifiedQueries } = state;
|
||||
const { query, index, override } = action.payload;
|
||||
|
||||
// Fast path: only change modifiedQueries to not trigger an update
|
||||
modifiedQueries[index] = query;
|
||||
if (!override) {
|
||||
return {
|
||||
...state,
|
||||
modifiedQueries,
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: changeQueryAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queries, queryTransactions } = state;
|
||||
const { query, index } = action.payload;
|
||||
|
||||
// Override path: queries are completely reset
|
||||
const nextQuery: DataQuery = {
|
||||
...query,
|
||||
...generateEmptyQuery(index),
|
||||
};
|
||||
const nextQueries = [...initialQueries];
|
||||
const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
|
||||
const nextQueries = [...queries];
|
||||
nextQueries[index] = nextQuery;
|
||||
modifiedQueries = [...nextQueries];
|
||||
|
||||
// Discard ongoing transaction related to row query
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
|
||||
return {
|
||||
...state,
|
||||
initialQueries: nextQueries,
|
||||
modifiedQueries: nextQueries.slice(),
|
||||
queries: nextQueries,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ChangeSize: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: changeSizeAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { range, datasourceInstance } = state;
|
||||
let interval = '1s';
|
||||
if (datasourceInstance && datasourceInstance.interval) {
|
||||
@ -137,67 +151,79 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
const containerWidth = action.payload.width;
|
||||
const queryIntervals = getIntervals(range, interval, containerWidth);
|
||||
return { ...state, containerWidth, queryIntervals };
|
||||
}
|
||||
|
||||
case ActionTypes.ChangeTime: {
|
||||
return {
|
||||
...state,
|
||||
range: action.payload.range,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ClearQueries: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: changeTimeAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return { ...state, range: action.payload.range };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: clearQueriesAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const queries = ensureQueries();
|
||||
return {
|
||||
...state,
|
||||
initialQueries: queries.slice(),
|
||||
modifiedQueries: queries.slice(),
|
||||
queries: queries.slice(),
|
||||
queryTransactions: [],
|
||||
showingStartPage: Boolean(state.StartPage),
|
||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.HighlightLogsExpression: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: highlightLogsExpressionAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { expressions } = action.payload;
|
||||
return { ...state, logsHighlighterExpressions: expressions };
|
||||
}
|
||||
|
||||
case ActionTypes.InitializeExplore: {
|
||||
const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: initializeExploreAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
containerWidth,
|
||||
eventBridge,
|
||||
exploreDatasources,
|
||||
range,
|
||||
initialQueries: queries,
|
||||
queries,
|
||||
initialized: true,
|
||||
modifiedQueries: queries.slice(),
|
||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||
...ui,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.UpdateDatasourceInstance: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: updateDatasourceInstanceAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { datasourceInstance } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
datasourceInstance,
|
||||
datasourceName: datasourceInstance.name,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourceFailure: {
|
||||
return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: loadDatasourceFailureAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourceMissing: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: loadDatasourceMissingAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
return { ...state, datasourceMissing: true, datasourceLoading: false };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourcePending: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: loadDatasourcePendingAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadDatasourceSuccess: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: loadDatasourceSuccessAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { containerWidth, range } = state;
|
||||
const {
|
||||
StartPage,
|
||||
@ -226,32 +252,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
logsHighlighterExpressions: undefined,
|
||||
queryTransactions: [],
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ModifyQueries: {
|
||||
const { initialQueries, modifiedQueries, queryTransactions } = state;
|
||||
const { modification, index, modifier } = action.payload as any;
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: modifyQueriesAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queries, queryTransactions } = state;
|
||||
const { modification, index, modifier } = action.payload;
|
||||
let nextQueries: DataQuery[];
|
||||
let nextQueryTransactions;
|
||||
if (index === undefined) {
|
||||
// Modify all queries
|
||||
nextQueries = initialQueries.map((query, i) => ({
|
||||
...modifier(modifiedQueries[i], modification),
|
||||
nextQueries = queries.map((query, i) => ({
|
||||
...modifier({ ...query }, modification),
|
||||
...generateEmptyQuery(i),
|
||||
}));
|
||||
// Discard all ongoing transactions
|
||||
nextQueryTransactions = [];
|
||||
} else {
|
||||
// Modify query only at index
|
||||
nextQueries = initialQueries.map((query, i) => {
|
||||
nextQueries = queries.map((query, i) => {
|
||||
// Synchronize all queries with local query cache to ensure consistency
|
||||
// TODO still needed?
|
||||
return i === index
|
||||
? {
|
||||
...modifier(modifiedQueries[i], modification),
|
||||
...generateEmptyQuery(i),
|
||||
}
|
||||
: query;
|
||||
return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
|
||||
});
|
||||
nextQueryTransactions = queryTransactions
|
||||
// Consume the hint corresponding to the action
|
||||
@ -266,22 +289,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
initialQueries: nextQueries,
|
||||
modifiedQueries: nextQueries.slice(),
|
||||
queries: nextQueries,
|
||||
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.QueryTransactionFailure: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: queryTransactionFailureAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queryTransactions } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
queryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.QueryTransactionStart: {
|
||||
return { ...state, queryTransactions, showingStartPage: false };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: queryTransactionStartAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queryTransactions } = state;
|
||||
const { resultType, rowIndex, transaction } = action.payload;
|
||||
// Discarding existing transactions of same type
|
||||
@ -292,14 +315,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
// Append new transaction
|
||||
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
|
||||
|
||||
return {
|
||||
...state,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.QueryTransactionSuccess: {
|
||||
return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: queryTransactionSuccessAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { datasourceInstance, queryIntervals } = state;
|
||||
const { history, queryTransactions } = action.payload;
|
||||
const results = calculateResultsFromQueryTransactions(
|
||||
@ -308,30 +329,24 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
queryIntervals.intervalMs
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...results,
|
||||
history,
|
||||
queryTransactions,
|
||||
showingStartPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.RemoveQueryRow: {
|
||||
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
|
||||
let { modifiedQueries } = state;
|
||||
return { ...state, ...results, history, queryTransactions, showingStartPage: false };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: removeQueryRowAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
|
||||
const { index } = action.payload;
|
||||
|
||||
modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
|
||||
|
||||
if (initialQueries.length <= 1) {
|
||||
if (queries.length <= 1) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
|
||||
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
|
||||
const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
|
||||
|
||||
// Discard transactions related to row query
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
|
||||
const results = calculateResultsFromQueryTransactions(
|
||||
nextQueryTransactions,
|
||||
datasourceInstance,
|
||||
@ -341,26 +356,34 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
return {
|
||||
...state,
|
||||
...results,
|
||||
initialQueries: nextQueries,
|
||||
queries: nextQueries,
|
||||
logsHighlighterExpressions: undefined,
|
||||
modifiedQueries: nextQueries.slice(),
|
||||
queryTransactions: nextQueryTransactions,
|
||||
queryKeys: nextQueryKeys,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.RunQueriesEmpty: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: runQueriesEmptyAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
return { ...state, queryTransactions: [] };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanRange: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: scanRangeAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return { ...state, scanRange: action.payload.range };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanStart: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: scanStartAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return { ...state, scanning: true, scanner: action.payload.scanner };
|
||||
}
|
||||
|
||||
case ActionTypes.ScanStop: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: scanStopAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const { queryTransactions } = state;
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
|
||||
return {
|
||||
@ -370,14 +393,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
scanRange: undefined,
|
||||
scanner: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.SetQueries: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: setQueriesAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queries } = action.payload;
|
||||
return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
|
||||
}
|
||||
|
||||
case ActionTypes.ToggleGraph: {
|
||||
return {
|
||||
...state,
|
||||
queries: queries.slice(),
|
||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||
};
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: toggleGraphAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const showingGraph = !state.showingGraph;
|
||||
let nextQueryTransactions = state.queryTransactions;
|
||||
if (!showingGraph) {
|
||||
@ -385,9 +416,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
|
||||
}
|
||||
return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
|
||||
}
|
||||
|
||||
case ActionTypes.ToggleLogs: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: toggleLogsAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const showingLogs = !state.showingLogs;
|
||||
let nextQueryTransactions = state.queryTransactions;
|
||||
if (!showingLogs) {
|
||||
@ -395,9 +428,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
|
||||
}
|
||||
return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
|
||||
}
|
||||
|
||||
case ActionTypes.ToggleTable: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: toggleTableAction,
|
||||
mapper: (state): ExploreItemState => {
|
||||
const showingTable = !state.showingTable;
|
||||
if (showingTable) {
|
||||
return { ...state, showingTable, queryTransactions: state.queryTransactions };
|
||||
@ -412,25 +447,26 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
||||
);
|
||||
|
||||
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
|
||||
}
|
||||
|
||||
case ActionTypes.QueriesImported: {
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: queriesImportedAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
const { queries } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
initialQueries: action.payload.queries,
|
||||
modifiedQueries: action.payload.queries.slice(),
|
||||
queries,
|
||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
},
|
||||
})
|
||||
.create();
|
||||
|
||||
/**
|
||||
* Global Explore reducer that handles multiple Explore areas (left and right).
|
||||
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
|
||||
*/
|
||||
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
|
||||
export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SplitClose: {
|
||||
return { ...state, split: false };
|
||||
@ -453,10 +489,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp
|
||||
const { exploreId } = action.payload as any;
|
||||
if (exploreId !== undefined) {
|
||||
const exploreItemState = state[exploreId];
|
||||
return {
|
||||
...state,
|
||||
[exploreId]: itemReducer(exploreItemState, action),
|
||||
};
|
||||
return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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([]);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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" />
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@ -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 |
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -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))));
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
25
vendor/github.com/aws/aws-sdk-go/aws/awsutil/string_value.go
generated
vendored
@ -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) + "}")
|
||||
|
2
vendor/github.com/aws/aws-sdk-go/aws/client/client.go
generated
vendored
@ -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
|
||||
}
|
||||
|
58
vendor/github.com/aws/aws-sdk-go/aws/config.go
generated
vendored
@ -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
|
||||
|
4
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/handlers.go
generated
vendored
@ -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
|
||||
}
|
||||
|
||||
|
2
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/user_agent.go
generated
vendored
@ -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.
|
||||
|
4
vendor/github.com/aws/aws-sdk-go/aws/credentials/chain_provider.go
generated
vendored
@ -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`,
|
||||
|
46
vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go
generated
vendored
@ -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
|
||||
}
|
||||
|
6
vendor/github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds/ec2_role_provider.go
generated
vendored
@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -12,6 +11,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/internal/sdkuri"
|
||||
)
|
||||
|
||||
// ProviderName provides a name of EC2Role provider
|
||||
@ -125,7 +125,7 @@ type ec2RoleCredRespBody struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
const iamSecurityCredsPath = "/iam/security-credentials"
|
||||
const iamSecurityCredsPath = "iam/security-credentials/"
|
||||
|
||||
// requestCredList requests a list of credentials from the EC2 service.
|
||||
// If there are no credentials, or there is an error making or receiving the request
|
||||
@ -153,7 +153,7 @@ func requestCredList(client *ec2metadata.EC2Metadata) ([]string, error) {
|
||||
// If the credentials cannot be found, or there is an error reading the response
|
||||
// and error will be returned.
|
||||
func requestCred(client *ec2metadata.EC2Metadata, credsName string) (ec2RoleCredRespBody, error) {
|
||||
resp, err := client.GetMetadata(path.Join(iamSecurityCredsPath, credsName))
|
||||
resp, err := client.GetMetadata(sdkuri.PathJoin(iamSecurityCredsPath, credsName))
|
||||
if err != nil {
|
||||
return ec2RoleCredRespBody{},
|
||||
awserr.New("EC2RoleRequestError",
|
||||
|
7
vendor/github.com/aws/aws-sdk-go/aws/credentials/endpointcreds/provider.go
generated
vendored
@ -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()
|
||||
}
|
||||
|
4
vendor/github.com/aws/aws-sdk-go/aws/credentials/env_provider.go
generated
vendored
@ -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)
|
||||
)
|
||||
|
||||
|