Merge branch 'master' into azure-monitor-refactor-#15087

This commit is contained in:
corpglory-dev
2019-02-06 14:31:23 +03:00
208 changed files with 29479 additions and 4137 deletions
+4
View File
@@ -2,6 +2,10 @@
### 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)
# 6.0.0-beta1 (2019-01-30)
Generated
+10 -2
View File
@@ -37,6 +37,7 @@
"aws/credentials",
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/processcreds",
"aws/credentials/stscreds",
"aws/csm",
"aws/defaults",
@@ -45,13 +46,18 @@
"aws/request",
"aws/session",
"aws/signer/v4",
"internal/ini",
"internal/s3err",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/ec2query",
"private/protocol/eventstream",
"private/protocol/eventstream/eventstreamapi",
"private/protocol/json/jsonutil",
"private/protocol/jsonrpc",
"private/protocol/query",
"private/protocol/query/queryutil",
"private/protocol/rest",
@@ -60,11 +66,13 @@
"service/cloudwatch",
"service/ec2",
"service/ec2/ec2iface",
"service/resourcegroupstaggingapi",
"service/resourcegroupstaggingapi/resourcegroupstaggingapiiface",
"service/s3",
"service/sts"
]
revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
version = "v1.14.12"
revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2"
version = "v1.16.15"
[[projects]]
branch = "master"
+38 -16
View File
@@ -25,49 +25,71 @@ the latest master builds [here](https://grafana.com/grafana/download)
### Dependencies
- Go (Latest Stable)
- bra [`go get github.com/Unknwon/bra`]
- Node.js LTS
- yarn [`npm install -g yarn`]
### Get the project
**The project located in the go-path will be your working directory.**
### Building the backend
```bash
go get github.com/grafana/grafana
cd $GOPATH/src/github.com/grafana/grafana
```
### Building
#### The backend
```bash
go run build.go setup
go run build.go build
```
### Building frontend assets
#### Frontend assets
For this you need Node.js (LTS version).
*For this you need Node.js (LTS version).*
To build the assets, rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
```bash
npm install -g yarn
yarn install --pure-lockfile
```
### Run and rebuild on source change
#### Backend
To run the backend and rebuild on source change:
```bash
$GOPATH/bin/bra run
```
#### Frontend
Rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
```bash
yarn watch
```
Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
```bash
yarn start
# OR set a theme
env GRAFANA_THEME=light yarn start
```
Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
Run tests
*Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.*
Run tests and rebuild on source change:
```bash
yarn jest
```
### Recompile backend on source change
To rebuild on source change.
```bash
go get github.com/Unknwon/bra
bra run
```
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
**Open grafana in your browser (default: e.g. `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).**
### Building a Docker image
+3
View File
@@ -113,6 +113,9 @@ cache_mode = private
# Login cookie name
cookie_name = grafana_session
# Login cookie same site setting. defaults to `lax`. can be set to "lax", "strict" and "none"
cookie_samesite = lax
# How many days an session can be unused before we inactivate it
login_remember_days = 7
+3
View File
@@ -109,6 +109,9 @@ log_queries =
# Login cookie name
;cookie_name = grafana_session
# Login cookie same site setting. defaults to `lax`. can be set to "lax", "strict" and "none"
;cookie_samesite = lax
# How many days an session can be unused before we inactivate it
;login_remember_days = 7
+27
View File
@@ -0,0 +1,27 @@
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
client:
url: http://loki:3100/api/prom/push
scrape_configs:
- job_name: system
entry_parser: raw
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log
- job_name: grafana
entry_parser: raw
static_configs:
- targets:
- localhost
labels:
job: grafana
__path__: /var/log/grafana/*log
+2 -10
View File
@@ -1,22 +1,14 @@
version: "3"
networks:
loki:
services:
loki:
image: grafana/loki:master
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- loki
promtail:
image: grafana/promtail:master
volumes:
- ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml
- /var/log:/var/log
- ../data/log:/var/log/grafana
command:
-config.file=/etc/promtail/docker-config.yaml
networks:
- loki
@@ -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
+1 -3
View File
@@ -393,9 +393,7 @@ Analytics ID here. By default this feature is disabled.
### check_for_updates
Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
send any sensitive information.
Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
<hr />
@@ -167,6 +167,7 @@ $arrowSize: 15px;
color: inherit;
padding: 0;
border-radius: 10px;
cursor: pointer;
}
.sp-replacer:hover,
@@ -2,7 +2,6 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
import { TimeSeriesVMs } from '../../types/data';
import { ValueMapping, MappingType } from '../../types';
jest.mock('jquery', () => ({
@@ -23,7 +22,7 @@ const setup = (propOverrides?: object) => {
stat: 'avg',
height: 300,
width: 300,
timeSeries: {} as TimeSeriesVMs,
value: 25,
decimals: 0,
};
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { ValueMapping, Threshold, BasicGaugeColor, TimeSeriesVMs, GrafanaTheme } from '../../types';
import { ValueMapping, Threshold, BasicGaugeColor, GrafanaTheme } from '../../types';
import { getMappedValue } from '../../utils/valueMappings';
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
@@ -14,7 +14,6 @@ export interface Props {
maxValue: number;
minValue: number;
prefix: string;
timeSeries: TimeSeriesVMs;
thresholds: Threshold[];
showThresholdMarkers: boolean;
showThresholdLabels: boolean;
@@ -22,6 +21,7 @@ export interface Props {
suffix: string;
unit: string;
width: number;
value: number;
theme?: GrafanaTheme;
}
@@ -122,25 +122,7 @@ export class Gauge extends PureComponent<Props> {
}
draw() {
const {
maxValue,
minValue,
timeSeries,
showThresholdLabels,
showThresholdMarkers,
width,
height,
stat,
theme,
} = this.props;
let value: TimeSeriesValue = '';
if (timeSeries[0]) {
value = timeSeries[0].stats[stat];
} else {
value = null;
}
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
const formattedValue = this.formatValue(value) as string;
const dimension = Math.min(width, height * 1.3);
@@ -194,7 +176,7 @@ export class Gauge extends PureComponent<Props> {
try {
$.plot(this.canvasElement, [plotSeries], options);
} catch (err) {
console.log('Gauge rendering error', err, options, timeSeries);
console.log('Gauge rendering error', err, options, value);
}
}
@@ -1,26 +1,38 @@
// Libraries
import React, { SFC } from 'react';
import React, { FunctionComponent } from 'react';
interface Props {
title?: string;
onClose?: () => void;
children: JSX.Element | JSX.Element[];
children: JSX.Element | JSX.Element[] | boolean;
onAdd?: () => void;
}
export const PanelOptionsGroup: SFC<Props> = props => {
export const PanelOptionsGroup: FunctionComponent<Props> = props => {
return (
<div className="panel-options-group">
{props.title && (
{props.onAdd ? (
<div className="panel-options-group__header">
{props.title}
{props.onClose && (
<button className="btn btn-link" onClick={props.onClose}>
<i className="fa fa-remove" />
</button>
)}
<button className="panel-options-group__add-btn" onClick={props.onAdd}>
<div className="panel-options-group__add-circle">
<i className="fa fa-plus" />
</div>
<span className="panel-options-group__title">{props.title}</span>
</button>
</div>
) : (
props.title && (
<div className="panel-options-group__header">
<span className="panel-options-group__title">{props.title}</span>
{props.onClose && (
<button className="btn btn-link" onClick={props.onClose}>
<i className="fa fa-remove" />
</button>
)}
</div>
)
)}
<div className="panel-options-group__body">{props.children}</div>
{props.children && <div className="panel-options-group__body">{props.children}</div>}
</div>
);
};
@@ -7,18 +7,57 @@
.panel-options-group__header {
padding: 4px 8px;
font-size: 1.1rem;
background: $panel-options-group-header-bg;
position: relative;
border-radius: $border-radius $border-radius 0 0;
display: flex;
align-items: center;
.btn {
position: absolute;
right: 0;
top: 0px;
top: 0;
}
}
.panel-options-group__add-btn {
background: none;
border: none;
display: flex;
align-items: center;
padding: 0;
&:hover {
.panel-options-group__add-circle {
background-color: $btn-success-bg;
color: $text-color-strong;
}
}
}
.panel-options-group__add-circle {
@include gradientBar($btn-success-bg, $btn-success-bg-hl, $text-color);
border-radius: 50px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 6px;
i {
position: relative;
top: 1px;
}
}
.panel-options-group__title {
font-size: 1.1rem;
position: relative;
top: 1px;
}
.panel-options-group__body {
padding: 20px;
@@ -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}
@@ -1,11 +1,11 @@
.thresholds {
margin-bottom: 10px;
margin-bottom: 20px;
}
.thresholds-row {
display: flex;
flex-direction: row;
height: 70px;
height: 62px;
}
.thresholds-row:first-child > .thresholds-row-color-indicator {
@@ -21,21 +21,21 @@
}
.thresholds-row-add-button {
@include buttonBackground($btn-success-bg, $btn-success-bg-hl, $text-color);
align-self: center;
margin-right: 5px;
color: $green;
height: 24px;
width: 24px;
background-color: $green;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.thresholds-row-add-button > i {
color: $white;
&:hover {
color: $text-color-strong;
}
}
.thresholds-row-color-indicator {
@@ -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')} />;
});
@@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
import { MappingType } from '../../types/panel';
import { MappingType } from '../../types';
const setup = (propOverrides?: object) => {
const props: Props = {
@@ -1,8 +1,8 @@
import React, { PureComponent } from 'react';
import MappingRow from './MappingRow';
import { MappingType, ValueMapping } from '../../types/panel';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { MappingType, ValueMapping } from '../../types';
import { PanelOptionsGroup } from '..';
export interface Props {
valueMappings: ValueMapping[];
@@ -81,8 +81,7 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
const { valueMappings } = this.state;
return (
<PanelOptionsGroup title="Value Mappings">
<div>
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
{valueMappings.length > 0 &&
valueMappings.map((valueMapping, index) => (
<MappingRow
@@ -92,13 +91,6 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
/>
))}
</div>
<div className="add-mapping-row" onClick={this.addMapping}>
<div className="add-mapping-row-icon">
<i className="fa fa-plus" />
</div>
<div className="add-mapping-row-label">Add mapping</div>
</div>
</PanelOptionsGroup>
);
}
@@ -2,55 +2,37 @@
exports[`Render should render component 1`] = `
<Component
title="Value Mappings"
onAdd={[Function]}
title="Add value mapping"
>
<div>
<MappingRow
key="Ok-0"
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"id": 1,
"operator": "",
"text": "Ok",
"type": 1,
"value": "20",
}
<MappingRow
key="Ok-0"
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"id": 1,
"operator": "",
"text": "Ok",
"type": 1,
"value": "20",
}
/>
<MappingRow
key="Meh-1"
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"from": "21",
"id": 2,
"operator": "",
"text": "Meh",
"to": "30",
"type": 2,
}
}
/>
<MappingRow
key="Meh-1"
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"from": "21",
"id": 2,
"operator": "",
"text": "Meh",
"to": "30",
"type": 2,
}
/>
</div>
<div
className="add-mapping-row"
onClick={[Function]}
>
<div
className="add-mapping-row-icon"
>
<i
className="fa fa-plus"
/>
</div>
<div
className="add-mapping-row-label"
>
Add mapping
</div>
</div>
}
/>
</Component>
`;
+1 -1
View File
@@ -3,7 +3,7 @@ import { PluginMeta } from './plugin';
import { TableData, TimeSeries } from './data';
export interface DataQueryResponse {
data: TimeSeries[] | [TableData];
data: TimeSeries[] | [TableData] | any;
}
export interface DataQuery {
+1 -1
View File
@@ -4,7 +4,7 @@ import { TimeRange } from './time';
export type InterpolateFunction = (value: string, format?: string | Function) => string;
export interface PanelProps<T = any> {
timeSeries: TimeSeries[];
panelData: PanelData;
timeRange: TimeRange;
loading: LoadingState;
options: T;
+25 -6
View File
@@ -1,6 +1,6 @@
import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel';
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
/**
@@ -41,22 +41,43 @@ 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;
onRunQuery: () => void;
onChange: (value: TQuery) => void;
}
export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
datasource: DSType;
query: TQuery;
error?: string | JSX.Element;
hint?: QueryHint;
history: any[];
onExecuteQuery?: () => void;
onQueryChange?: (value: TQuery) => void;
onExecuteHint?: (action: QueryFixAction) => void;
}
export interface ExploreStartPageProps {
onClickExample: (query: DataQuery) => void;
}
export interface PluginExports {
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;
}
+2 -2
View File
@@ -108,8 +108,8 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
// api renew session based on remember cookie
r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
// api renew session based on cookie
r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing))
// authed api
r.Group("/api", func(apiRoute routing.RouteRegister) {
+1 -1
View File
@@ -149,4 +149,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC
return nil
}
func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil }
+1 -1
View File
@@ -136,7 +136,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
}
func (hs *HTTPServer) Logout(c *m.ReqContext) {
hs.AuthTokenService.UserSignedOutHook(c)
hs.AuthTokenService.SignOutUser(c)
if setting.SignoutRedirectUrl != "" {
c.Redirect(setting.SignoutRedirectUrl)
+1 -1
View File
@@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC
return nil
}
func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil }
+16 -3
View File
@@ -3,6 +3,7 @@ package auth
import (
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"net/url"
"time"
@@ -31,7 +32,7 @@ var (
type UserAuthTokenService interface {
InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
UserSignedOutHook(c *models.ReqContext)
SignOutUser(c *models.ReqContext) error
}
type UserAuthTokenServiceImpl struct {
@@ -85,7 +86,7 @@ func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext,
func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
if setting.Env == setting.DEV {
ctx.Logger.Info("new token", "unhashed token", value)
ctx.Logger.Debug("new token", "unhashed token", value)
}
ctx.Resp.Header().Del("Set-Cookie")
@@ -96,6 +97,7 @@ func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, va
Path: setting.AppSubUrl + "/",
Secure: s.Cfg.SecurityHTTPSCookies,
MaxAge: maxAge,
SameSite: s.Cfg.LoginCookieSameSite,
}
http.SetCookie(ctx.Resp, &cookie)
@@ -111,8 +113,19 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m
return nil
}
func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) {
func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error {
unhashedToken := c.GetCookie(s.Cfg.LoginCookieName)
if unhashedToken == "" {
return errors.New("cannot logout without session token")
}
hashedToken := hashToken(unhashedToken)
sql := `DELETE FROM user_auth_token WHERE auth_token = ?`
_, err := s.SQLStore.NewSession().Exec(sql, hashedToken)
s.writeSessionCookie(c, "", -1)
return err
}
func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
+39
View File
@@ -1,10 +1,15 @@
package auth
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -46,6 +51,40 @@ func TestUserAuthToken(t *testing.T) {
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(LookupToken, ShouldBeNil)
})
Convey("signing out should delete token and cookie if present", func() {
httpreq := &http.Request{Header: make(http.Header)}
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken})
ctx := &models.ReqContext{Context: &macaron.Context{
Req: macaron.Request{Request: httpreq},
Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
},
Logger: log.New("fakelogger"),
}
err = userAuthTokenService.SignOutUser(ctx)
So(err, ShouldBeNil)
// makes sure we tell the browser to overwrite the cookie
cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName)
So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader)
})
Convey("signing out an none existing session should return an error", func() {
httpreq := &http.Request{Header: make(http.Header)}
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""})
ctx := &models.ReqContext{Context: &macaron.Context{
Req: macaron.Request{Request: httpreq},
Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
},
Logger: log.New("fakelogger"),
}
err = userAuthTokenService.SignOutUser(ctx)
So(err, ShouldNotBeNil)
})
})
Convey("expires correctly", func() {
+1 -4
View File
@@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
cnnstr += ss.buildExtraConnectionString('&')
case migrator.POSTGRES:
host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432")
if err != nil {
return "", err
}
host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432")
if ss.dbCfg.Pwd == "" {
ss.dbCfg.Pwd = "''"
}
+16
View File
@@ -6,6 +6,7 @@ package setting
import (
"bytes"
"fmt"
"net/http"
"net/url"
"os"
"path"
@@ -227,6 +228,7 @@ type Cfg struct {
LoginCookieMaxDays int
LoginCookieRotation int
LoginDeleteExpiredTokensAfterDays int
LoginCookieSameSite http.SameSite
SecurityHTTPSCookies bool
}
@@ -557,6 +559,20 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30)
samesiteString := login.Key("cookie_samesite").MustString("lax")
validSameSiteValues := map[string]http.SameSite{
"lax": http.SameSiteLaxMode,
"strict": http.SameSiteStrictMode,
"none": http.SameSiteDefaultMode,
}
if samesite, ok := validSameSiteValues[samesiteString]; ok {
cfg.LoginCookieSameSite = samesite
} else {
cfg.LoginCookieSameSite = http.SameSiteLaxMode
}
cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10)
if cfg.LoginCookieRotation < 2 {
cfg.LoginCookieRotation = 2
+3 -1
View File
@@ -21,6 +21,7 @@ import (
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/metrics"
@@ -28,7 +29,8 @@ import (
type CloudWatchExecutor struct {
*models.DataSource
ec2Svc ec2iface.EC2API
ec2Svc ec2iface.EC2API
rgtaSvc resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
}
type DatasourceInfo struct {
+87 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/tsdb"
@@ -95,10 +96,11 @@ func init() {
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
"AWS/ML": {"PredictCount", "PredictFailureCount"},
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
"AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"},
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
@@ -149,6 +151,7 @@ func init() {
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
"AWS/ML": {"MLModelId", "RequestMode"},
"AWS/NATGateway": {"NatGatewayId"},
"AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"},
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
"AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
@@ -198,6 +201,8 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
case "ec2_instance_attribute":
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
case "resource_arns":
data, err = e.handleGetResourceArns(ctx, parameters, queryContext)
}
transformToTable(data, queryResult)
@@ -534,6 +539,65 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
return result, nil
}
func (e *CloudWatchExecutor) ensureRGTAClientSession(region string) error {
if e.rgtaSvc == nil {
dsInfo := e.getDsInfo(region)
cfg, err := e.getAwsConfig(dsInfo)
if err != nil {
return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
}
sess, err := session.NewSession(cfg)
if err != nil {
return fmt.Errorf("Failed to call ec2:NewSession, %v", err)
}
e.rgtaSvc = resourcegroupstaggingapi.New(sess, cfg)
}
return nil
}
func (e *CloudWatchExecutor) handleGetResourceArns(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
region := parameters.Get("region").MustString()
resourceType := parameters.Get("resourceType").MustString()
filterJson := parameters.Get("tags").MustMap()
err := e.ensureRGTAClientSession(region)
if err != nil {
return nil, err
}
var filters []*resourcegroupstaggingapi.TagFilter
for k, v := range filterJson {
if vv, ok := v.([]interface{}); ok {
var vvvvv []*string
for _, vvv := range vv {
if vvvv, ok := vvv.(string); ok {
vvvvv = append(vvvvv, &vvvv)
}
}
filters = append(filters, &resourcegroupstaggingapi.TagFilter{
Key: aws.String(k),
Values: vvvvv,
})
}
}
var resourceTypes []*string
resourceTypes = append(resourceTypes, &resourceType)
resources, err := e.resourceGroupsGetResources(region, filters, resourceTypes)
if err != nil {
return nil, err
}
result := make([]suggestData, 0)
for _, resource := range resources.ResourceTagMappingList {
data := *resource.ResourceARN
result = append(result, suggestData{Text: data, Value: data})
}
return result, nil
}
func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
svc, err := e.getClient(region)
if err != nil {
@@ -585,6 +649,28 @@ func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.
return &resp, nil
}
func (e *CloudWatchExecutor) resourceGroupsGetResources(region string, filters []*resourcegroupstaggingapi.TagFilter, resourceTypes []*string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
params := &resourcegroupstaggingapi.GetResourcesInput{
ResourceTypeFilters: resourceTypes,
TagFilters: filters,
}
var resp resourcegroupstaggingapi.GetResourcesOutput
err := e.rgtaSvc.GetResourcesPages(params,
func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
resources, _ := awsutil.ValuesAtPath(page, "ResourceTagMappingList")
for _, resource := range resources {
resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, resource.(*resourcegroupstaggingapi.ResourceTagMapping))
}
return !lastPage
})
if err != nil {
return nil, errors.New("Failed to call tags:GetResources")
}
return &resp, nil
}
func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
creds, err := GetCredentials(cwData)
if err != nil {
@@ -8,6 +8,8 @@ import (
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/bmizerany/assert"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -22,6 +24,11 @@ type mockedEc2 struct {
RespRegions ec2.DescribeRegionsOutput
}
type mockedRGTA struct {
resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
Resp resourcegroupstaggingapi.GetResourcesOutput
}
func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
fn(&m.Resp, true)
return nil
@@ -30,6 +37,11 @@ func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeR
return &m.RespRegions, nil
}
func (m mockedRGTA) GetResourcesPages(in *resourcegroupstaggingapi.GetResourcesInput, fn func(*resourcegroupstaggingapi.GetResourcesOutput, bool) bool) error {
fn(&m.Resp, true)
return nil
}
func TestCloudWatchMetrics(t *testing.T) {
Convey("When calling getMetricsForCustomMetrics", t, func() {
@@ -209,6 +221,51 @@ func TestCloudWatchMetrics(t *testing.T) {
So(result[7].Text, ShouldEqual, "vol-4-2")
})
})
Convey("When calling handleGetResourceArns", t, func() {
executor := &CloudWatchExecutor{
rgtaSvc: mockedRGTA{
Resp: resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: []*resourcegroupstaggingapi.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567"),
Tags: []*resourcegroupstaggingapi.Tag{
{
Key: aws.String("Environment"),
Value: aws.String("production"),
},
},
},
{
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321"),
Tags: []*resourcegroupstaggingapi.Tag{
{
Key: aws.String("Environment"),
Value: aws.String("production"),
},
},
},
},
},
},
}
json := simplejson.New()
json.Set("region", "us-east-1")
json.Set("resourceType", "ec2:instance")
tags := make(map[string]interface{})
tags["Environment"] = []string{"production"}
json.Set("tags", tags)
result, _ := executor.handleGetResourceArns(context.Background(), json, &tsdb.TsdbQuery{})
Convey("Should return all two instances", func() {
So(result[0].Text, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567")
So(result[0].Value, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567")
So(result[1].Text, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321")
So(result[1].Value, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321")
})
})
}
func TestParseMultiSelectValue(t *testing.T) {
+1 -4
View File
@@ -49,10 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
}
}
server, port, err := util.SplitIPPort(datasource.Url, "1433")
if err != nil {
return "", err
}
server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
-25
View File
@@ -1,25 +0,0 @@
package util
import (
"net"
)
// SplitIPPort splits the ip string and port.
func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) {
ipAddr := net.ParseIP(ipStr)
if ipAddr == nil {
// Port was included
ip, port, err = net.SplitHostPort(ipStr)
if err != nil {
return "", "", err
}
} else {
// No port was included
ip = ipAddr.String()
port = portDefault
}
return ip, port, nil
}
+36 -11
View File
@@ -7,23 +7,48 @@ 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"
}
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, "", "")
}
+83 -1
View File
@@ -9,8 +9,90 @@ import (
func TestParseIPAddress(t *testing.T) {
Convey("Test parse ip address", t, func() {
So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1")
})
}
func TestSplitHostPortDefault(t *testing.T) {
Convey("Test split ip address to host and port", t, func() {
host, port := SplitHostPortDefault("192.168.0.140:456", "", "")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "456")
host, port = SplitHostPortDefault("192.168.0.140", "", "123")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("[::1:456]", "", "")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "456")
host, port = SplitHostPortDefault("[::1]", "", "123")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("::1:123", "", "")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("::1", "", "123")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault(":456", "1.2.3.4", "")
So(host, ShouldEqual, "1.2.3.4")
So(port, ShouldEqual, "456")
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "123")
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "123")
})
}
func TestSplitHostPort(t *testing.T) {
Convey("Test split ip address to host and port", t, func() {
host, port := SplitHostPort("192.168.0.140:456")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "456")
host, port = SplitHostPort("192.168.0.140")
So(host, ShouldEqual, "192.168.0.140")
So(port, ShouldEqual, "")
host, port = SplitHostPort("[::1:456]")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "456")
host, port = SplitHostPort("[::1]")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "")
host, port = SplitHostPort("::1:123")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "123")
host, port = SplitHostPort("::1")
So(host, ShouldEqual, "::1")
So(port, ShouldEqual, "")
host, port = SplitHostPort(":456")
So(host, ShouldEqual, "")
So(port, ShouldEqual, "456")
host, port = SplitHostPort("xyz.rds.amazonaws.com")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "")
host, port = SplitHostPort("xyz.rds.amazonaws.com:123")
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
So(port, ShouldEqual, "123")
})
}
-43
View File
@@ -1,43 +0,0 @@
package util
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestSplitIPPort(t *testing.T) {
Convey("When parsing an IPv4 without explicit port", t, func() {
ip, port, err := SplitIPPort("1.2.3.4", "5678")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "1.2.3.4")
So(port, ShouldEqual, "5678")
})
Convey("When parsing an IPv6 without explicit port", t, func() {
ip, port, err := SplitIPPort("::1", "5678")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "::1")
So(port, ShouldEqual, "5678")
})
Convey("When parsing an IPv4 with explicit port", t, func() {
ip, port, err := SplitIPPort("1.2.3.4:56", "78")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "1.2.3.4")
So(port, ShouldEqual, "56")
})
Convey("When parsing an IPv6 with explicit port", t, func() {
ip, port, err := SplitIPPort("[::1]:56", "78")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "::1")
So(port, ShouldEqual, "56")
})
}
@@ -1,8 +1,7 @@
import React from 'react';
import _ from 'lodash';
import { Select } from '@grafana/ui';
import { SelectOptionItem } from '@grafana/ui';
import { Select, SelectOptionItem } from '@grafana/ui';
import { Variable } from 'app/types/templates';
export interface Props {
+2 -3
View File
@@ -1,7 +1,6 @@
import _ from 'lodash';
import { colors } from '@grafana/ui';
import { TimeSeries } from 'app/core/core';
import { colors, TimeSeries } from '@grafana/ui';
import { getThemeColor } from 'app/core/utils/colors';
/**
@@ -341,6 +340,6 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
return a[1] - b[1];
});
return new TimeSeries(series);
return { datapoints: series.datapoints, target: series.alias, color: series.color };
});
}
+26 -11
View File
@@ -1,6 +1,7 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export class BackendSrv {
@@ -103,10 +104,17 @@ export class BackendSrv {
err => {
// handle unauthorized
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
return this.loginPing().then(() => {
options.retry = 1;
return this.request(options);
});
return this.loginPing()
.then(() => {
options.retry = 1;
return this.request(options);
})
.catch(err => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
this.$timeout(this.requestErrorHandler.bind(this, err), 50);
@@ -184,13 +192,20 @@ export class BackendSrv {
// handle unauthorized for backend requests
if (requestIsLocal && firstAttempt && err.status === 401) {
return this.loginPing().then(() => {
options.retry = 1;
if (canceler) {
canceler.resolve();
}
return this.datasourceRequest(options);
});
return this.loginPing()
.then(() => {
options.retry = 1;
if (canceler) {
canceler.resolve();
}
return this.datasourceRequest(options);
})
.catch(err => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
// populate error obj on Internal Error
+1 -1
View File
@@ -249,7 +249,7 @@ export class KeybindingSrv {
if (panelInfo.panel.legend) {
const panelRef = dashboard.getPanelById(dashboard.meta.focusPanelId);
panelRef.legend.show = !panelRef.legend.show;
panelRef.refresh();
panelRef.render();
}
}
});
+30 -2
View File
@@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null,
queries: [],
range: DEFAULT_RANGE,
ui: {
showingGraph: true,
showingTable: true,
showingLogs: true,
}
};
describe('state functions', () => {
@@ -69,9 +74,11 @@ describe('state functions', () => {
to: 'now',
},
};
expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}'
);
});
@@ -93,7 +100,7 @@ describe('state functions', () => {
},
};
expect(serializeStateToUrlParam(state, true)).toBe(
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]'
);
});
});
@@ -118,7 +125,28 @@ describe('state functions', () => {
};
const serialized = serializeStateToUrlParam(state);
const parsed = parseUrlState(serialized);
expect(state).toMatchObject(parsed);
});
it('can parse the compact serialized state into the original state', () => {
const state = {
...DEFAULT_EXPLORE_STATE,
datasource: 'foo',
queries: [
{
expr: 'metric{test="a/b"}',
},
{
expr: 'super{foo="x/z"}',
},
],
range: {
from: 'now - 5h',
to: 'now',
},
};
const serialized = serializeStateToUrlParam(state, true);
const parsed = parseUrlState(serialized);
expect(state).toMatchObject(parsed);
});
});
+46 -5
View File
@@ -11,7 +11,7 @@ import { colors } from '@grafana/ui';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
// Types
import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types';
import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui/src/types';
import TimeSeries from 'app/core/time_series2';
import {
ExploreUrlState,
@@ -27,6 +27,12 @@ export const DEFAULT_RANGE = {
to: 'now',
};
export const DEFAULT_UI_STATE = {
showingTable: true,
showingGraph: true,
showingLogs: true,
};
const MAX_HISTORY_ITEMS = 100;
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
@@ -147,7 +153,12 @@ export function buildQueryTransaction(
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
export function parseUrlState(initial: string | undefined): ExploreUrlState {
let uiState = DEFAULT_UI_STATE;
if (initial) {
try {
const parsed = JSON.parse(decodeURI(initial));
@@ -160,20 +171,41 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
to: parsed[1],
};
const datasource = parsed[2];
const queries = parsed.slice(3);
return { datasource, queries, range };
let queries = [];
parsed.slice(3).forEach(segment => {
if (isMetricSegment(segment)) {
queries = [...queries, segment];
}
if (isUISegment(segment)) {
uiState = {
showingGraph: segment.ui[0],
showingLogs: segment.ui[1],
showingTable: segment.ui[2],
};
}
});
return { datasource, queries, range, ui: uiState };
}
return parsed;
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: uiState };
}
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
if (compact) {
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
return JSON.stringify([
urlState.range.from,
urlState.range.to,
urlState.datasource,
...urlState.queries,
{ ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] },
]);
}
return JSON.stringify(urlState);
}
@@ -304,3 +336,12 @@ export function clearHistory(datasourceId: string) {
const historyKey = `grafana.explore.history.${datasourceId}`;
store.delete(historyKey);
}
export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => {
const queryKeys = queries.reduce((newQueryKeys, query, index) => {
const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
return newQueryKeys.concat(`${primaryKey}-${index}`);
}, []);
return queryKeys;
};
@@ -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
</button>
<div className="add-panel-widget__create">
{this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)}
{this.renderOptionLink('visualization', 'Choose Visualization', () =>
this.onCreateNewPanel('visualization')
)}
</div>
<div className="add-panel-widget__actions">
<button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>Convert to row</button>
{copiedPanelPlugins.length === 1 && (
<button
className="btn btn-inverse add-panel-widget__action"
onClick={() => this.onPasteCopiedPanel(copiedPanelPlugins[0])}
>
Paste copied panel
</button>
)}
</div>
</div>
</div>
</div>
@@ -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';
@@ -94,7 +94,7 @@ export class PanelChrome extends PureComponent<Props, State> {
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
}
renderPanel(loading, timeSeries, width, height): JSX.Element {
renderPanel(loading, panelData, width, height): JSX.Element {
const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.Panel;
@@ -109,7 +109,7 @@ export class PanelChrome extends PureComponent<Props, State> {
<div className="panel-content">
<PanelComponent
loading={loading}
timeSeries={timeSeries}
panelData={panelData}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.PanelDefaults)}
width={width - 2 * variables.panelHorizontalPadding}
@@ -158,7 +158,7 @@ export class PanelChrome extends PureComponent<Props, State> {
onDataResponse={this.onDataResponse}
>
{({ loading, panelData }) => {
return this.renderPanel(loading, panelData.timeSeries, width, height);
return this.renderPanel(loading, panelData, width, height);
}}
</DataPanel>
)}
@@ -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
@@ -165,6 +165,11 @@ export class QueriesTab extends PureComponent<Props, State> {
this.setState({ isAddingMixed: false });
};
onQueryChange = (query: DataQuery, index) => {
this.props.panel.changeQuery(query, index);
this.forceUpdate();
};
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement;
this.setState({ scrollTop: target.scrollTop });
@@ -201,6 +206,7 @@ export class QueriesTab extends PureComponent<Props, State> {
key={query.refId}
panel={panel}
query={query}
onChange={query => this.onQueryChange(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.onAddQuery}
onMoveQuery={this.onMoveQuery}
@@ -18,6 +18,7 @@ interface Props {
onAddQuery: (query?: DataQuery) => void;
onRemoveQuery: (query: DataQuery) => void;
onMoveQuery: (query: DataQuery, direction: number) => void;
onChange: (query: DataQuery) => void;
dataSourceValue: string | null;
inMixedMode: boolean;
}
@@ -105,17 +106,12 @@ export class QueryEditorRow extends PureComponent<Props, State> {
this.setState({ isCollapsed: !this.state.isCollapsed });
};
onQueryChange = (query: DataQuery) => {
Object.assign(this.props.query, query);
this.onExecuteQuery();
};
onExecuteQuery = () => {
onRunQuery = () => {
this.props.panel.refresh();
};
renderPluginEditor() {
const { query } = this.props;
const { query, onChange } = this.props;
const { datasource } = this.state;
if (datasource.pluginExports.QueryCtrl) {
@@ -128,8 +124,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
<QueryEditor
query={query}
datasource={datasource}
onQueryChange={this.onQueryChange}
onExecuteQuery={this.onExecuteQuery}
onChange={onChange}
onRunQuery={this.onRunQuery}
/>
);
}
@@ -166,7 +162,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
onDisableQuery = () => {
this.props.query.hide = !this.props.query.hide;
this.onExecuteQuery();
this.onRunQuery();
this.forceUpdate();
};
@@ -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);
@@ -269,6 +269,19 @@ export class PanelModel {
});
}
changeQuery(query: DataQuery, index: number) {
// ensure refId is maintained
query.refId = this.targets[index].refId;
// update query in array
this.targets = this.targets.map((item, itemIndex) => {
if (itemIndex === index) {
return query;
}
return item;
});
}
destroy() {
this.events.emit('panel-teardown');
this.events.removeAllListeners();
@@ -16,12 +16,12 @@ export class DataSourcesListItem extends PureComponent<Props> {
</div>
<div className="card-item-body">
<figure className="card-item-figure">
<img src={dataSource.typeLogoUrl} />
<img src={dataSource.typeLogoUrl} alt={dataSource.name} />
</figure>
<div className="card-item-details">
<div className="card-item-name">
{dataSource.name}
{dataSource.isDefault && <span className="btn btn-secondary btn-mini">default</span>}
{dataSource.isDefault && <span className="btn btn-secondary btn-mini card-item-label">default</span>}
</div>
<div className="card-item-sub-name">{dataSource.url}</div>
</div>
@@ -24,6 +24,7 @@ exports[`Render should render component 1`] = `
className="card-item-figure"
>
<img
alt="gdev-cloudwatch"
src="public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"
/>
</figure>
+20 -25
View File
@@ -1,5 +1,5 @@
// Libraries
import React from 'react';
import React, { ComponentClass } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import _ from 'lodash';
@@ -18,34 +18,26 @@ import TableContainer from './TableContainer';
import TimePicker, { parseTime } from './TimePicker';
// Actions
import {
changeSize,
changeTime,
initializeExplore,
modifyQueries,
scanStart,
scanStop,
setQueries,
} from './state/actions';
import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
// Types
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
import { StoreState } from 'app/types';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter';
import { ExploreToolbar } from './ExploreToolbar';
import { scanStopAction } from './state/actionTypes';
interface ExploreProps {
StartPage?: any;
StartPage?: ComponentClass<ExploreStartPageProps>;
changeSize: typeof changeSize;
changeTime: typeof changeTime;
datasourceError: string;
datasourceInstance: any;
datasourceInstance: ExploreDataSourceApi;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
exploreId: ExploreId;
initialQueries: DataQuery[];
initializeExplore: typeof initializeExplore;
initialized: boolean;
modifyQueries: typeof modifyQueries;
@@ -54,7 +46,7 @@ interface ExploreProps {
scanning?: boolean;
scanRange?: RawTimeRange;
scanStart: typeof scanStart;
scanStop: typeof scanStop;
scanStopAction: typeof scanStopAction;
setQueries: typeof setQueries;
split: boolean;
showingStartPage?: boolean;
@@ -62,6 +54,7 @@ interface ExploreProps {
supportsLogs: boolean | null;
supportsTable: boolean | null;
urlState: ExploreUrlState;
queryKeys: string[];
}
/**
@@ -107,18 +100,20 @@ export class Explore extends React.PureComponent<ExploreProps> {
// Don't initialize on split, but need to initialize urlparameters when present
if (!initialized) {
// Load URL state and parse range
const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState;
const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
const initialQueries: DataQuery[] = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
const width = this.el ? this.el.offsetWidth : 0;
this.props.initializeExplore(
exploreId,
initialDatasource,
initialQueries,
initialRange,
width,
this.exploreEvents
this.exploreEvents,
ui
);
}
}
@@ -171,7 +166,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
};
onStopScanning = () => {
this.props.scanStop(this.props.exploreId);
this.props.scanStopAction({ exploreId: this.props.exploreId });
};
render() {
@@ -182,12 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps> {
datasourceLoading,
datasourceMissing,
exploreId,
initialQueries,
showingStartPage,
split,
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
} = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore';
@@ -208,7 +203,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
{datasourceInstance &&
!datasourceError && (
<div className="explore-container">
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} initialQueries={initialQueries} />
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
<AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => (
<main className="m-t-2" style={{ width }}>
@@ -216,7 +211,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && <GraphContainer exploreId={exploreId} />}
{supportsGraph && !supportsLogs && <GraphContainer 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">
<TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
<TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
</ClickOutsideWrapper>
</div>
<div className="explore-toolbar-content-item">
<button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>
+4 -1
View File
@@ -3,6 +3,8 @@ import React, { PureComponent } from 'react';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import {
LogsDedupDescription,
LogsDedupStrategy,
@@ -205,12 +207,13 @@ export default class Logs extends PureComponent<Props, State> {
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
const getRows = () => processedRows;
const timeSeries = data.series.map(series => new TimeSeries(series));
return (
<div className="logs-panel">
<div className="logs-panel-graph">
<Graph
data={data.series}
data={timeSeries}
height="100px"
range={range}
id={`explore-logs-graph-${exploreId}`}
+4 -7
View File
@@ -14,7 +14,7 @@ interface QueryEditorProps {
datasource: any;
error?: string | JSX.Element;
onExecuteQuery?: () => void;
onQueryChange?: (value: DataQuery, override?: boolean) => void;
onQueryChange?: (value: DataQuery) => void;
initialQuery: DataQuery;
exploreEvents: Emitter;
range: RawTimeRange;
@@ -40,20 +40,17 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
datasource,
target,
refresh: () => {
this.props.onQueryChange(target, false);
this.props.onQueryChange(target);
this.props.onExecuteQuery();
},
events: exploreEvents,
panel: {
datasource,
targets: [target],
},
panel: { datasource, targets: [target] },
dashboard: {},
},
};
this.component = loader.load(this.element, scopeProps, template);
this.props.onQueryChange(target, false);
this.props.onQueryChange(target);
}
componentWillUnmount() {
+81 -54
View File
@@ -33,10 +33,9 @@ export interface QueryFieldProps {
cleanText?: (text: string) => string;
disabled?: boolean;
initialQuery: string | null;
onBlur?: () => void;
onFocus?: () => void;
onExecuteQuery?: () => void;
onQueryChange?: (value: string) => void;
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
onValueChanged?: (value: string) => void;
onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
placeholder?: string;
portalOrigin?: string;
@@ -51,6 +50,7 @@ export interface QueryFieldState {
typeaheadPrefix: string;
typeaheadText: string;
value: Value;
lastExecutedValue: Value;
}
export interface TypeaheadInput {
@@ -90,6 +90,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
typeaheadPrefix: '',
typeaheadText: '',
value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
lastExecutedValue: null,
};
}
@@ -104,11 +105,20 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
}
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
const { initialQuery, syntax } = this.props;
const { value, suggestions } = this.state;
// if query changed from the outside
if (initialQuery !== prevProps.initialQuery) {
// and we have a version that differs
if (initialQuery !== Plain.serialize(value)) {
this.placeholdersBuffer = new PlaceholdersBuffer(initialQuery || '');
this.setState({ value: makeValue(this.placeholdersBuffer.toString(), syntax) });
}
}
// Only update menu location when suggestion existence or text/selection changed
if (
this.state.value !== prevState.value ||
hasSuggestions(this.state.suggestions) !== hasSuggestions(prevState.suggestions)
) {
if (value !== prevState.value || hasSuggestions(suggestions) !== hasSuggestions(prevState.suggestions)) {
this.updateMenu();
}
}
@@ -123,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;
@@ -135,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();
}
}
});
@@ -150,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 });
}
};
@@ -279,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': {
@@ -303,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;
}
@@ -355,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 = () => {
@@ -450,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', {
@@ -466,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}
+28 -29
View File
@@ -9,20 +9,14 @@ import QueryEditor from './QueryEditor';
import QueryTransactionStatus from './QueryTransactionStatus';
// Actions
import {
addQueryRow,
changeQuery,
highlightLogsExpression,
modifyQueries,
removeQueryRow,
runQueries,
} from './state/actions';
import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions';
// Types
import { StoreState } from 'app/types';
import { RawTimeRange, DataQuery, QueryHint } from '@grafana/ui';
import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui';
import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter';
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@@ -37,16 +31,16 @@ interface QueryRowProps {
changeQuery: typeof changeQuery;
className?: string;
exploreId: ExploreId;
datasourceInstance: any;
highlightLogsExpression: typeof highlightLogsExpression;
datasourceInstance: ExploreDataSourceApi;
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
history: HistoryItem[];
index: number;
initialQuery: DataQuery;
query: DataQuery;
modifyQueries: typeof modifyQueries;
queryTransactions: QueryTransaction[];
exploreEvents: Emitter;
range: RawTimeRange;
removeQueryRow: typeof removeQueryRow;
removeQueryRowAction: typeof removeQueryRowAction;
runQueries: typeof runQueries;
}
@@ -65,6 +59,10 @@ export class QueryRow extends PureComponent<QueryRowProps> {
}
};
componentWillUnmount() {
console.log('QueryRow will unmount');
}
onClickAddButton = () => {
const { exploreId, index } = this.props;
this.props.addQueryRow(exploreId, index);
@@ -74,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);
@@ -107,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}
/>
) : (
@@ -125,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" />
@@ -156,17 +155,17 @@ export class QueryRow extends PureComponent<QueryRowProps> {
function mapStateToProps(state: StoreState, { exploreId, index }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
const initialQuery = initialQueries[index];
return { datasourceInstance, history, initialQuery, queryTransactions, range };
const { datasourceInstance, history, queries, queryTransactions, range } = item;
const query = queries[index];
return { datasourceInstance, history, query, queryTransactions, range };
}
const mapDispatchToProps = {
addQueryRow,
changeQuery,
highlightLogsExpression,
highlightLogsExpressionAction,
modifyQueries,
removeQueryRow,
removeQueryRowAction,
runQueries,
};
+5 -7
View File
@@ -6,25 +6,23 @@ import QueryRow from './QueryRow';
// Types
import { Emitter } from 'app/core/utils/emitter';
import { DataQuery } from '@grafana/ui/src/types';
import { ExploreId } from 'app/types/explore';
interface QueryRowsProps {
className?: string;
exploreEvents: Emitter;
exploreId: ExploreId;
initialQueries: DataQuery[];
queryKeys: string[];
}
export default class QueryRows extends PureComponent<QueryRowsProps> {
render() {
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
return (
<div className={className}>
{initialQueries.map((query, index) => (
// TODO instead of relying on initialQueries, move to react key list in redux
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
))}
{queryKeys.map((key, index) => {
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
})}
</div>
);
}
+7 -7
View File
@@ -7,16 +7,16 @@ import { StoreState } from 'app/types';
import { ExploreId, ExploreUrlState } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
import { initializeExploreSplit, resetExplore } from './state/actions';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
import { CustomScrollbar } from '@grafana/ui';
import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
interface WrapperProps {
initializeExploreSplit: typeof initializeExploreSplit;
initializeExploreSplitAction: typeof initializeExploreSplitAction;
split: boolean;
updateLocation: typeof updateLocation;
resetExplore: typeof resetExplore;
resetExploreAction: typeof resetExploreAction;
urlStates: { [key: string]: string };
}
@@ -39,12 +39,12 @@ export class Wrapper extends Component<WrapperProps> {
componentDidMount() {
if (this.initialSplit) {
this.props.initializeExploreSplit();
this.props.initializeExploreSplitAction();
}
}
componentWillUnmount() {
this.props.resetExplore();
this.props.resetExploreAction();
}
render() {
@@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => {
};
const mapDispatchToProps = {
initializeExploreSplit,
initializeExploreSplitAction,
updateLocation,
resetExplore,
resetExploreAction,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
+386 -280
View File
@@ -1,6 +1,13 @@
// Types
import { Emitter } from 'app/core/core';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi } from '@grafana/ui/src/types';
import {
RawTimeRange,
TimeRange,
DataQuery,
DataSourceSelectItem,
DataSourceApi,
QueryFixAction,
} from '@grafana/ui/src/types';
import {
ExploreId,
ExploreItemState,
@@ -8,234 +15,28 @@ import {
RangeScanner,
ResultType,
QueryTransaction,
ExploreUIState,
} from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
/** Higher order actions
*
*/
export enum ActionTypes {
AddQueryRow = 'explore/ADD_QUERY_ROW',
ChangeDatasource = 'explore/CHANGE_DATASOURCE',
ChangeQuery = 'explore/CHANGE_QUERY',
ChangeSize = 'explore/CHANGE_SIZE',
ChangeTime = 'explore/CHANGE_TIME',
ClearQueries = 'explore/CLEAR_QUERIES',
HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
InitializeExplore = 'explore/INITIALIZE_EXPLORE',
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
ModifyQueries = 'explore/MODIFY_QUERIES',
QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
RunQueries = 'explore/RUN_QUERIES',
RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
ScanRange = 'explore/SCAN_RANGE',
ScanStart = 'explore/SCAN_START',
ScanStop = 'explore/SCAN_STOP',
SetQueries = 'explore/SET_QUERIES',
SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN',
StateSave = 'explore/STATE_SAVE',
ToggleGraph = 'explore/TOGGLE_GRAPH',
ToggleLogs = 'explore/TOGGLE_LOGS',
ToggleTable = 'explore/TOGGLE_TABLE',
UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE',
ResetExplore = 'explore/RESET_EXPLORE',
QueriesImported = 'explore/QueriesImported',
}
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
payload: {
exploreId: ExploreId;
index: number;
query: DataQuery;
};
}
export interface ChangeQueryAction {
type: ActionTypes.ChangeQuery;
payload: {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
};
}
export interface ChangeSizeAction {
type: ActionTypes.ChangeSize;
payload: {
exploreId: ExploreId;
width: number;
height: number;
};
}
export interface ChangeTimeAction {
type: ActionTypes.ChangeTime;
payload: {
exploreId: ExploreId;
range: TimeRange;
};
}
export interface ClearQueriesAction {
type: ActionTypes.ClearQueries;
payload: {
exploreId: ExploreId;
};
}
export interface HighlightLogsExpressionAction {
type: ActionTypes.HighlightLogsExpression;
payload: {
exploreId: ExploreId;
expressions: string[];
};
}
export interface InitializeExploreAction {
type: ActionTypes.InitializeExplore;
payload: {
exploreId: ExploreId;
containerWidth: number;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
};
}
export interface InitializeExploreSplitAction {
type: ActionTypes.InitializeExploreSplit;
}
export interface LoadDatasourceFailureAction {
type: ActionTypes.LoadDatasourceFailure;
payload: {
exploreId: ExploreId;
error: string;
};
}
export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
payload: {
exploreId: ExploreId;
requestedDatasourceName: string;
};
}
export interface LoadDatasourceMissingAction {
type: ActionTypes.LoadDatasourceMissing;
payload: {
exploreId: ExploreId;
};
}
export interface LoadDatasourceSuccessAction {
type: ActionTypes.LoadDatasourceSuccess;
payload: {
exploreId: ExploreId;
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
logsHighlighterExpressions?: any[];
showingStartPage: boolean;
supportsGraph: boolean;
supportsLogs: boolean;
supportsTable: boolean;
};
}
export interface ModifyQueriesAction {
type: ActionTypes.ModifyQueries;
payload: {
exploreId: ExploreId;
modification: any;
index: number;
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
};
}
export interface QueryTransactionFailureAction {
type: ActionTypes.QueryTransactionFailure;
payload: {
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
};
}
export interface QueryTransactionStartAction {
type: ActionTypes.QueryTransactionStart;
payload: {
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
};
}
export interface QueryTransactionSuccessAction {
type: ActionTypes.QueryTransactionSuccess;
payload: {
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
};
}
export interface RemoveQueryRowAction {
type: ActionTypes.RemoveQueryRow;
payload: {
exploreId: ExploreId;
index: number;
};
}
export interface RunQueriesEmptyAction {
type: ActionTypes.RunQueriesEmpty;
payload: {
exploreId: ExploreId;
};
}
export interface ScanStartAction {
type: ActionTypes.ScanStart;
payload: {
exploreId: ExploreId;
scanner: RangeScanner;
};
}
export interface ScanRangeAction {
type: ActionTypes.ScanRange;
payload: {
exploreId: ExploreId;
range: RawTimeRange;
};
}
export interface ScanStopAction {
type: ActionTypes.ScanStop;
payload: {
exploreId: ExploreId;
};
}
export interface SetQueriesAction {
type: ActionTypes.SetQueries;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
payload: {};
}
export interface SplitCloseAction {
type: ActionTypes.SplitClose;
payload: {};
}
export interface SplitOpenAction {
@@ -245,80 +46,385 @@ export interface SplitOpenAction {
};
}
export interface StateSaveAction {
type: ActionTypes.StateSave;
}
export interface ToggleTableAction {
type: ActionTypes.ToggleTable;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleGraphAction {
type: ActionTypes.ToggleGraph;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleLogsAction {
type: ActionTypes.ToggleLogs;
payload: {
exploreId: ExploreId;
};
}
export interface UpdateDatasourceInstanceAction {
type: ActionTypes.UpdateDatasourceInstance;
payload: {
exploreId: ExploreId;
datasourceInstance: DataSourceApi;
};
}
export interface ResetExploreAction {
type: ActionTypes.ResetExplore;
payload: {};
}
export interface QueriesImported {
type: ActionTypes.QueriesImported;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
/** Lower order actions
*
*/
export interface AddQueryRowPayload {
exploreId: ExploreId;
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>;
+163 -253
View File
@@ -30,40 +30,54 @@ import {
DataQuery,
DataSourceSelectItem,
QueryHint,
QueryFixAction,
} from '@grafana/ui/src/types';
import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions, ExploreUIState } from 'app/types/explore';
import {
ExploreId,
ExploreUrlState,
RangeScanner,
ResultType,
QueryOptions,
QueryTransaction,
} from 'app/types/explore';
import {
Action as ThunkableAction,
ActionTypes,
AddQueryRowAction,
ChangeSizeAction,
HighlightLogsExpressionAction,
LoadDatasourceFailureAction,
LoadDatasourceMissingAction,
LoadDatasourcePendingAction,
LoadDatasourceSuccessAction,
QueryTransactionStartAction,
ScanStopAction,
UpdateDatasourceInstanceAction,
QueriesImported,
Action,
updateDatasourceInstanceAction,
changeQueryAction,
changeSizeAction,
ChangeSizePayload,
changeTimeAction,
scanStopAction,
clearQueriesAction,
initializeExploreAction,
loadDatasourceMissingAction,
loadDatasourceFailureAction,
loadDatasourcePendingAction,
queriesImportedAction,
LoadDatasourceSuccessPayload,
loadDatasourceSuccessAction,
modifyQueriesAction,
queryTransactionFailureAction,
queryTransactionStartAction,
queryTransactionSuccessAction,
scanRangeAction,
runQueriesEmptyAction,
scanStartAction,
setQueriesAction,
splitCloseAction,
splitOpenAction,
addQueryRowAction,
AddQueryRowPayload,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
ToggleGraphPayload,
ToggleLogsPayload,
ToggleTablePayload,
} from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
/**
* Adds a query row after the row with the given index.
*/
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
// /**
// * Adds a query row after the row with the given index.
// */
export function addQueryRow(exploreId: ExploreId, index: number): ActionOf<AddQueryRowPayload> {
const query = generateEmptyQuery(index + 1);
return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
return addQueryRowAction({ exploreId, index, query });
}
/**
@@ -73,12 +87,20 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
return async (dispatch, getState) => {
const newDataSourceInstance = await getDatasourceSrv().get(datasource);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
const modifiedQueries = getState().explore[exploreId].modifiedQueries;
const queries = getState().explore[exploreId].queries;
await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
dispatch(loadDatasource(exploreId, newDataSourceInstance));
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
try {
await dispatch(loadDatasource(exploreId, newDataSourceInstance));
} catch (error) {
console.error(error);
return;
}
dispatch(runQueries(exploreId));
};
}
@@ -98,7 +120,7 @@ export function changeQuery(
query = { ...generateEmptyQuery(index) };
}
dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
dispatch(changeQueryAction({ exploreId, query, index, override }));
if (override) {
dispatch(runQueries(exploreId));
}
@@ -112,8 +134,8 @@ export function changeQuery(
export function changeSize(
exploreId: ExploreId,
{ height, width }: { height: number; width: number }
): ChangeSizeAction {
return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
): ActionOf<ChangeSizePayload> {
return changeSizeAction({ exploreId, height, width });
}
/**
@@ -121,7 +143,7 @@ export function changeSize(
*/
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
dispatch(changeTimeAction({ exploreId, range }));
dispatch(runQueries(exploreId));
};
}
@@ -131,19 +153,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
*/
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
return dispatch => {
dispatch(scanStop(exploreId));
dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
dispatch(scanStopAction({ exploreId }));
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave());
};
}
/**
* Highlight expressions in the log results
*/
export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
}
/**
* Initialize Explore state with state from the URL and the React component.
* Call this only on components for with the Explore state has not been initialized.
@@ -154,7 +169,8 @@ export function initializeExplore(
queries: DataQuery[],
range: RawTimeRange,
containerWidth: number,
eventBridge: Emitter
eventBridge: Emitter,
ui: ExploreUIState
): ThunkResult<void> {
return async dispatch => {
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
@@ -165,18 +181,17 @@ export function initializeExplore(
meta: ds.meta,
}));
dispatch({
type: ActionTypes.InitializeExplore,
payload: {
dispatch(
initializeExploreAction({
exploreId,
containerWidth,
datasourceName,
eventBridge,
exploreDatasources,
queries,
range,
},
});
ui,
})
);
if (exploreDatasources.length >= 1) {
let instance;
@@ -193,75 +208,27 @@ export function initializeExplore(
instance = await getDatasourceSrv().get();
}
dispatch(updateDatasourceInstance(exploreId, instance));
dispatch(loadDatasource(exploreId, instance));
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance }));
try {
await dispatch(loadDatasource(exploreId, instance));
} catch (error) {
console.error(error);
return;
}
dispatch(runQueries(exploreId, true));
} else {
dispatch(loadDatasourceMissing(exploreId));
dispatch(loadDatasourceMissingAction({ exploreId }));
}
};
}
/**
* Initialize the wrapper split state
*/
export function initializeExploreSplit() {
return async dispatch => {
dispatch({ type: ActionTypes.InitializeExploreSplit });
};
}
/**
* Display an error that happened during the selection of a datasource
*/
export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
type: ActionTypes.LoadDatasourceFailure,
payload: {
exploreId,
error,
},
});
/**
* Display an error when no datasources have been configured
*/
export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
type: ActionTypes.LoadDatasourceMissing,
payload: { exploreId },
});
/**
* Start the async process of loading a datasource to display a loading indicator
*/
export const loadDatasourcePending = (
exploreId: ExploreId,
requestedDatasourceName: string
): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
payload: {
exploreId,
requestedDatasourceName,
},
});
export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => {
return {
type: ActionTypes.QueriesImported,
payload: {
exploreId,
queries,
},
};
};
/**
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
* e.g., Prometheus -> Loki queries.
*/
export const loadDatasourceSuccess = (
exploreId: ExploreId,
instance: any,
): LoadDatasourceSuccessAction => {
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf<LoadDatasourceSuccessPayload> => {
// Capabilities
const supportsGraph = instance.meta.metrics;
const supportsLogs = instance.meta.logs;
@@ -274,37 +241,18 @@ export const loadDatasourceSuccess = (
// Save last-used datasource
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
return {
type: ActionTypes.LoadDatasourceSuccess,
payload: {
exploreId,
StartPage,
datasourceInstance: instance,
history,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
supportsTable,
},
};
return loadDatasourceSuccessAction({
exploreId,
StartPage,
datasourceInstance: instance,
history,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
supportsTable,
});
};
/**
* Updates datasource instance before datasouce loading has started
*/
export function updateDatasourceInstance(
exploreId: ExploreId,
instance: DataSourceApi
): UpdateDatasourceInstanceAction {
return {
type: ActionTypes.UpdateDatasourceInstance,
payload: {
exploreId,
datasourceInstance: instance,
},
};
}
export function importQueries(
exploreId: ExploreId,
queries: DataQuery[],
@@ -326,11 +274,11 @@ export function importQueries(
}
const nextQueries = importedQueries.map((q, i) => ({
...importedQueries[i],
...q,
...generateEmptyQuery(i),
}));
dispatch(queriesImported(exploreId, nextQueries));
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
};
}
@@ -342,9 +290,9 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
const datasourceName = instance.name;
// Keep ID to track selection
dispatch(loadDatasourcePending(exploreId, datasourceName));
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
let datasourceError = null;
try {
const testResult = await instance.testDatasource();
datasourceError = testResult.status === 'success' ? null : testResult.message;
@@ -353,8 +301,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
}
if (datasourceError) {
dispatch(loadDatasourceFailure(exploreId, datasourceError));
return;
dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
return Promise.reject(`${datasourceName} loading failed`);
}
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
@@ -372,7 +320,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
}
dispatch(loadDatasourceSuccess(exploreId, instance));
dispatch(runQueries(exploreId));
return Promise.resolve();
};
}
@@ -385,12 +333,12 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
*/
export function modifyQueries(
exploreId: ExploreId,
modification: any,
modification: QueryFixAction,
index: number,
modifier: any
): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
if (!modification.preventSubmit) {
dispatch(runQueries(exploreId));
}
@@ -455,29 +403,10 @@ export function queryTransactionFailure(
return qt;
});
dispatch({
type: ActionTypes.QueryTransactionFailure,
payload: { exploreId, queryTransactions: nextQueryTransactions },
});
dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
};
}
/**
* Start a query transaction for the given result type.
* @param exploreId Explore area
* @param transaction Query options and `done` status.
* @param resultType Associate the transaction with a result viewer, e.g., Graph
* @param rowIndex Index is used to associate latency for this transaction with a query row
*/
export function queryTransactionStart(
exploreId: ExploreId,
transaction: QueryTransaction,
resultType: ResultType,
rowIndex: number
): QueryTransactionStartAction {
return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
}
/**
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
* If the transaction was started by a scanner, it keeps on scanning for more results.
@@ -534,14 +463,13 @@ export function queryTransactionSuccess(
// Side-effect: Saving history in localstorage
const nextHistory = updateHistory(history, datasourceId, queries);
dispatch({
type: ActionTypes.QueryTransactionSuccess,
payload: {
dispatch(
queryTransactionSuccessAction({
exploreId,
history: nextHistory,
queryTransactions: nextQueryTransactions,
},
});
})
);
// Keep scanning for results if this was the last scanning transaction
if (scanning) {
@@ -549,34 +477,24 @@ export function queryTransactionSuccess(
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) {
const range = scanner();
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
dispatch(scanRangeAction({ exploreId, range }));
}
} else {
// We can stop scanning if we have a result
dispatch(scanStop(exploreId));
dispatch(scanStopAction({ exploreId }));
}
}
};
}
/**
* Remove query row of the given index, as well as associated query results.
*/
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
dispatch(runQueries(exploreId));
};
}
/**
* Main action to run queries and dispatches sub-actions based on which result viewers are active
*/
export function runQueries(exploreId: ExploreId) {
export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
return (dispatch, getState) => {
const {
datasourceInstance,
modifiedQueries,
queries,
showingLogs,
showingGraph,
showingTable,
@@ -585,8 +503,8 @@ export function runQueries(exploreId: ExploreId) {
supportsTable,
} = getState().explore[exploreId];
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
if (!hasNonEmptyQuery(queries)) {
dispatch(runQueriesEmptyAction({ exploreId }));
dispatch(stateSave()); // Remember to saves to state and update location
return;
}
@@ -596,7 +514,7 @@ export function runQueries(exploreId: ExploreId) {
const interval = datasourceInstance.interval;
// Keep table queries first since they need to return quickly
if (showingTable && supportsTable) {
if ((ignoreUIState || showingTable) && supportsTable) {
dispatch(
runQueriesForType(
exploreId,
@@ -611,7 +529,7 @@ export function runQueries(exploreId: ExploreId) {
)
);
}
if (showingGraph && supportsGraph) {
if ((ignoreUIState || showingGraph) && supportsGraph) {
dispatch(
runQueriesForType(
exploreId,
@@ -625,9 +543,10 @@ export function runQueries(exploreId: ExploreId) {
)
);
}
if (showingLogs && supportsLogs) {
if ((ignoreUIState || showingLogs) && supportsLogs) {
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
}
dispatch(stateSave());
};
}
@@ -646,14 +565,7 @@ function runQueriesForType(
resultGetter?: any
) {
return async (dispatch, getState) => {
const {
datasourceInstance,
eventBridge,
modifiedQueries: queries,
queryIntervals,
range,
scanning,
} = getState().explore[exploreId];
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
const datasourceId = datasourceInstance.meta.id;
// Run all queries concurrently
@@ -667,7 +579,7 @@ function runQueriesForType(
queryIntervals,
scanning
);
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
try {
const now = Date.now();
const res = await datasourceInstance.query(transaction.options);
@@ -691,21 +603,14 @@ function runQueriesForType(
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
return dispatch => {
// Register the scanner
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
dispatch(scanStartAction({ exploreId, scanner }));
// Scanning must trigger query run, and return the new range
const range = scanner();
// Set the new range to be displayed
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
dispatch(scanRangeAction({ exploreId, range }));
};
}
/**
* Stop any scanning for more results.
*/
export function scanStop(exploreId: ExploreId): ScanStopAction {
return { type: ActionTypes.ScanStop, payload: { exploreId } };
}
/**
* Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run.
@@ -714,13 +619,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
return dispatch => {
// Inject react keys into query objects
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
dispatch({
type: ActionTypes.SetQueries,
payload: {
exploreId,
queries,
},
});
dispatch(setQueriesAction({ exploreId, queries }));
dispatch(runQueries(exploreId));
};
}
@@ -730,7 +629,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
*/
export function splitClose(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.SplitClose });
dispatch(splitCloseAction());
dispatch(stateSave());
};
}
@@ -747,9 +646,9 @@ export function splitOpen(): ThunkResult<void> {
const itemState = {
...leftState,
queryTransactions: [],
initialQueries: leftState.modifiedQueries.slice(),
queries: leftState.queries.slice(),
};
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
dispatch(splitOpenAction({ itemState }));
dispatch(stateSave());
};
}
@@ -764,63 +663,74 @@ export function stateSave() {
const urlStates: { [index: string]: string } = {};
const leftUrlState: ExploreUrlState = {
datasource: left.datasourceInstance.name,
queries: left.modifiedQueries.map(clearQueryKeys),
queries: left.queries.map(clearQueryKeys),
range: left.range,
ui: {
showingGraph: left.showingGraph,
showingLogs: left.showingLogs,
showingTable: left.showingTable,
},
};
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
if (split) {
const rightUrlState: ExploreUrlState = {
datasource: right.datasourceInstance.name,
queries: right.modifiedQueries.map(clearQueryKeys),
queries: right.queries.map(clearQueryKeys),
range: right.range,
ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable },
};
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
}
dispatch(updateLocation({ query: urlStates }));
};
}
/**
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
* Creates action to collapse graph/logs/table panel. When panel is collapsed,
* queries won't be run
*/
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
const togglePanelActionCreator = (
actionCreator:
| ActionCreator<ToggleGraphPayload>
| ActionCreator<ToggleLogsPayload>
| ActionCreator<ToggleTablePayload>
) => (exploreId: ExploreId) => {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
if (getState().explore[exploreId].showingGraph) {
let shouldRunQueries;
dispatch(actionCreator({ exploreId }));
dispatch(stateSave());
switch (actionCreator.type) {
case toggleGraphAction.type:
shouldRunQueries = getState().explore[exploreId].showingGraph;
break;
case toggleLogsAction.type:
shouldRunQueries = getState().explore[exploreId].showingLogs;
break;
case toggleTableAction.type:
shouldRunQueries = getState().explore[exploreId].showingTable;
break;
}
if (shouldRunQueries) {
dispatch(runQueries(exploreId));
}
};
}
};
/**
* Expand/collapse the 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,
});
});
});
});
+212 -179
View File
@@ -3,11 +3,41 @@ import {
generateEmptyQuery,
getIntervals,
ensureQueries,
getQueryKeys,
} from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
import { DataQuery } from '@grafana/ui/src/types';
import { Action, ActionTypes } from './actionTypes';
import { HigherOrderAction, ActionTypes } from './actionTypes';
import { reducerFactory } from 'app/core/redux';
import {
addQueryRowAction,
changeQueryAction,
changeSizeAction,
changeTimeAction,
clearQueriesAction,
highlightLogsExpressionAction,
initializeExploreAction,
updateDatasourceInstanceAction,
loadDatasourceFailureAction,
loadDatasourceMissingAction,
loadDatasourcePendingAction,
loadDatasourceSuccessAction,
modifyQueriesAction,
queryTransactionFailureAction,
queryTransactionStartAction,
queryTransactionSuccessAction,
removeQueryRowAction,
runQueriesEmptyAction,
scanRangeAction,
scanStartAction,
scanStopAction,
setQueriesAction,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
queriesImportedAction,
} from './actionTypes';
export const DEFAULT_RANGE = {
from: 'now-6h',
@@ -30,9 +60,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
datasourceMissing: false,
exploreDatasources: [],
history: [],
initialQueries: [],
queries: [],
initialized: false,
modifiedQueries: [],
queryTransactions: [],
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
range: DEFAULT_RANGE,
@@ -44,6 +73,7 @@ export const makeExploreItemState = (): ExploreItemState => ({
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
queryKeys: [],
});
/**
@@ -58,21 +88,15 @@ export const initialExploreState: ExploreState = {
/**
* Reducer for an Explore area, to be used by the global Explore reducer.
*/
export const itemReducer = (state, action: Action): ExploreItemState => {
switch (action.type) {
case ActionTypes.AddQueryRow: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
.addMapper({
filter: addQueryRowAction,
mapper: (state, action): ExploreItemState => {
const { queries, queryTransactions } = state;
const { index, query } = action.payload;
// Add new query row after given index, keep modifications of existing rows
const nextModifiedQueries = [
...modifiedQueries.slice(0, index + 1),
{ ...query },
...initialQueries.slice(index + 1),
];
// Add to initialQueries, which will cause a new row to be rendered
const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
// Add to queries, which will cause a new row to be rendered
const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
// Ongoing transactions need to update their row indices
const nextQueryTransactions = queryTransactions.map(qt => {
@@ -87,48 +111,38 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
return {
...state,
initialQueries: nextQueries,
queries: nextQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: nextModifiedQueries,
queryTransactions: nextQueryTransactions,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
};
}
case ActionTypes.ChangeQuery: {
const { initialQueries, queryTransactions } = state;
let { modifiedQueries } = state;
const { query, index, override } = action.payload;
// Fast path: only change modifiedQueries to not trigger an update
modifiedQueries[index] = query;
if (!override) {
return {
...state,
modifiedQueries,
};
}
},
})
.addMapper({
filter: changeQueryAction,
mapper: (state, action): ExploreItemState => {
const { queries, queryTransactions } = state;
const { query, index } = action.payload;
// Override path: queries are completely reset
const nextQuery: DataQuery = {
...query,
...generateEmptyQuery(index),
};
const nextQueries = [...initialQueries];
const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
modifiedQueries = [...nextQueries];
// Discard ongoing transaction related to row query
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
return {
...state,
initialQueries: nextQueries,
modifiedQueries: nextQueries.slice(),
queries: nextQueries,
queryTransactions: nextQueryTransactions,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
};
}
case ActionTypes.ChangeSize: {
},
})
.addMapper({
filter: changeSizeAction,
mapper: (state, action): ExploreItemState => {
const { range, datasourceInstance } = state;
let interval = '1s';
if (datasourceInstance && datasourceInstance.interval) {
@@ -137,67 +151,79 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
const containerWidth = action.payload.width;
const queryIntervals = getIntervals(range, interval, containerWidth);
return { ...state, containerWidth, queryIntervals };
}
case ActionTypes.ChangeTime: {
return {
...state,
range: action.payload.range,
};
}
case ActionTypes.ClearQueries: {
},
})
.addMapper({
filter: changeTimeAction,
mapper: (state, action): ExploreItemState => {
return { ...state, range: action.payload.range };
},
})
.addMapper({
filter: clearQueriesAction,
mapper: (state): ExploreItemState => {
const queries = ensureQueries();
return {
...state,
initialQueries: queries.slice(),
modifiedQueries: queries.slice(),
queries: queries.slice(),
queryTransactions: [],
showingStartPage: Boolean(state.StartPage),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
};
}
case ActionTypes.HighlightLogsExpression: {
},
})
.addMapper({
filter: highlightLogsExpressionAction,
mapper: (state, action): ExploreItemState => {
const { expressions } = action.payload;
return { ...state, logsHighlighterExpressions: expressions };
}
case ActionTypes.InitializeExplore: {
const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
},
})
.addMapper({
filter: initializeExploreAction,
mapper: (state, action): ExploreItemState => {
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
return {
...state,
containerWidth,
eventBridge,
exploreDatasources,
range,
initialQueries: queries,
queries,
initialized: true,
modifiedQueries: queries.slice(),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
...ui,
};
}
case ActionTypes.UpdateDatasourceInstance: {
},
})
.addMapper({
filter: updateDatasourceInstanceAction,
mapper: (state, action): ExploreItemState => {
const { datasourceInstance } = action.payload;
return {
...state,
datasourceInstance,
datasourceName: datasourceInstance.name,
};
}
case ActionTypes.LoadDatasourceFailure: {
return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
},
})
.addMapper({
filter: loadDatasourceFailureAction,
mapper: (state, action): ExploreItemState => {
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
}
case ActionTypes.LoadDatasourceMissing: {
},
})
.addMapper({
filter: loadDatasourceMissingAction,
mapper: (state): ExploreItemState => {
return { ...state, datasourceMissing: true, datasourceLoading: false };
}
case ActionTypes.LoadDatasourcePending: {
},
})
.addMapper({
filter: loadDatasourcePendingAction,
mapper: (state, action): ExploreItemState => {
return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
}
case ActionTypes.LoadDatasourceSuccess: {
},
})
.addMapper({
filter: loadDatasourceSuccessAction,
mapper: (state, action): ExploreItemState => {
const { containerWidth, range } = state;
const {
StartPage,
@@ -226,32 +252,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
logsHighlighterExpressions: undefined,
queryTransactions: [],
};
}
case ActionTypes.ModifyQueries: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
const { modification, index, modifier } = action.payload as any;
},
})
.addMapper({
filter: modifyQueriesAction,
mapper: (state, action): ExploreItemState => {
const { queries, queryTransactions } = state;
const { modification, index, modifier } = action.payload;
let nextQueries: DataQuery[];
let nextQueryTransactions;
if (index === undefined) {
// Modify all queries
nextQueries = initialQueries.map((query, i) => ({
...modifier(modifiedQueries[i], modification),
nextQueries = queries.map((query, i) => ({
...modifier({ ...query }, modification),
...generateEmptyQuery(i),
}));
// Discard all ongoing transactions
nextQueryTransactions = [];
} else {
// Modify query only at index
nextQueries = initialQueries.map((query, i) => {
nextQueries = queries.map((query, i) => {
// Synchronize all queries with local query cache to ensure consistency
// TODO still needed?
return i === index
? {
...modifier(modifiedQueries[i], modification),
...generateEmptyQuery(i),
}
: query;
return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
});
nextQueryTransactions = queryTransactions
// Consume the hint corresponding to the action
@@ -266,22 +289,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
}
return {
...state,
initialQueries: nextQueries,
modifiedQueries: nextQueries.slice(),
queries: nextQueries,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.QueryTransactionFailure: {
},
})
.addMapper({
filter: queryTransactionFailureAction,
mapper: (state, action): ExploreItemState => {
const { queryTransactions } = action.payload;
return {
...state,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionStart: {
return { ...state, queryTransactions, showingStartPage: false };
},
})
.addMapper({
filter: queryTransactionStartAction,
mapper: (state, action): ExploreItemState => {
const { queryTransactions } = state;
const { resultType, rowIndex, transaction } = action.payload;
// Discarding existing transactions of same type
@@ -292,14 +315,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
// Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
return {
...state,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionSuccess: {
return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
},
})
.addMapper({
filter: queryTransactionSuccessAction,
mapper: (state, action): ExploreItemState => {
const { datasourceInstance, queryIntervals } = state;
const { history, queryTransactions } = action.payload;
const results = calculateResultsFromQueryTransactions(
@@ -308,30 +329,24 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
queryIntervals.intervalMs
);
return {
...state,
...results,
history,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.RemoveQueryRow: {
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
let { modifiedQueries } = state;
return { ...state, ...results, history, queryTransactions, showingStartPage: false };
},
})
.addMapper({
filter: removeQueryRowAction,
mapper: (state, action): ExploreItemState => {
const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
const { index } = action.payload;
modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
if (initialQueries.length <= 1) {
if (queries.length <= 1) {
return state;
}
const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
// Discard transactions related to row query
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
const results = calculateResultsFromQueryTransactions(
nextQueryTransactions,
datasourceInstance,
@@ -341,26 +356,34 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
return {
...state,
...results,
initialQueries: nextQueries,
queries: nextQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: nextQueries.slice(),
queryTransactions: nextQueryTransactions,
queryKeys: nextQueryKeys,
};
}
case ActionTypes.RunQueriesEmpty: {
},
})
.addMapper({
filter: runQueriesEmptyAction,
mapper: (state): ExploreItemState => {
return { ...state, queryTransactions: [] };
}
case ActionTypes.ScanRange: {
},
})
.addMapper({
filter: scanRangeAction,
mapper: (state, action): ExploreItemState => {
return { ...state, scanRange: action.payload.range };
}
case ActionTypes.ScanStart: {
},
})
.addMapper({
filter: scanStartAction,
mapper: (state, action): ExploreItemState => {
return { ...state, scanning: true, scanner: action.payload.scanner };
}
case ActionTypes.ScanStop: {
},
})
.addMapper({
filter: scanStopAction,
mapper: (state): ExploreItemState => {
const { queryTransactions } = state;
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
return {
@@ -370,14 +393,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
scanRange: undefined,
scanner: undefined,
};
}
case ActionTypes.SetQueries: {
},
})
.addMapper({
filter: setQueriesAction,
mapper: (state, action): ExploreItemState => {
const { queries } = action.payload;
return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
}
case ActionTypes.ToggleGraph: {
return {
...state,
queries: queries.slice(),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
};
},
})
.addMapper({
filter: toggleGraphAction,
mapper: (state): ExploreItemState => {
const showingGraph = !state.showingGraph;
let nextQueryTransactions = state.queryTransactions;
if (!showingGraph) {
@@ -385,9 +416,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
}
return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
}
case ActionTypes.ToggleLogs: {
},
})
.addMapper({
filter: toggleLogsAction,
mapper: (state): ExploreItemState => {
const showingLogs = !state.showingLogs;
let nextQueryTransactions = state.queryTransactions;
if (!showingLogs) {
@@ -395,9 +428,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
}
return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
}
case ActionTypes.ToggleTable: {
},
})
.addMapper({
filter: toggleTableAction,
mapper: (state): ExploreItemState => {
const showingTable = !state.showingTable;
if (showingTable) {
return { ...state, showingTable, queryTransactions: state.queryTransactions };
@@ -412,25 +447,26 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
);
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
}
case ActionTypes.QueriesImported: {
},
})
.addMapper({
filter: queriesImportedAction,
mapper: (state, action): ExploreItemState => {
const { queries } = action.payload;
return {
...state,
initialQueries: action.payload.queries,
modifiedQueries: action.payload.queries.slice(),
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) };
}
}
@@ -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;
@@ -0,0 +1,86 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { Select, SelectOptionItem } from '@grafana/ui';
// Types
import { QueryEditorProps } from '@grafana/ui/src/types';
import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types';
import { LokiQueryField } from './LokiQueryField';
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
interface State {
query: LokiQuery;
}
export class LokiQueryEditor extends PureComponent<Props> {
state: State = {
query: this.props.query,
};
onRunQuery = () => {
const { query } = this.state;
this.props.onChange(query);
this.props.onRunQuery();
};
onFieldChange = (query: LokiQuery, override?) => {
this.setState({
query: {
...this.state.query,
expr: query.expr,
},
});
};
onFormatChanged = (option: SelectOptionItem) => {
this.props.onChange({
...this.state.query,
resultFormat: option.value,
});
};
render() {
const { query } = this.state;
const { datasource } = this.props;
const formatOptions: SelectOptionItem[] = [
{ label: 'Time Series', value: 'time_series' },
{ label: 'Table', value: 'table' },
];
query.resultFormat = query.resultFormat || 'time_series';
const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
return (
<div>
<LokiQueryField
datasource={datasource}
query={query}
onQueryChange={this.onFieldChange}
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}
/>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</div>
);
}
}
export default LokiQueryEditor;
@@ -15,8 +15,9 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
// 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';
@@ -64,15 +65,8 @@ interface CascaderOption {
disabled?: boolean;
}
interface LokiQueryFieldProps {
datasource: any;
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 {
@@ -80,7 +74,7 @@ interface LokiQueryFieldState {
syntaxLoaded: boolean;
}
class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
plugins: any[];
pluginsSearch: any[];
languageProvider: any;
@@ -97,14 +91,14 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
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: [],
@@ -168,20 +162,21 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
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);
}
};
@@ -219,7 +214,7 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
};
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;
@@ -239,10 +234,11 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
<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">
@@ -7,6 +7,17 @@ describe('LokiDatasource', () => {
url: 'myloggingurl',
};
const testResp = {
data: {
streams: [
{
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
labels: '{}',
},
],
},
};
describe('when querying', () => {
const backendSrvMock = { datasourceRequest: jest.fn() };
@@ -17,7 +28,7 @@ describe('LokiDatasource', () => {
test('should use default max lines when no limit given', () => {
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn();
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
ds.query(options);
@@ -30,7 +41,7 @@ describe('LokiDatasource', () => {
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
const customSettings = { ...instanceSettings, jsonData: customData };
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn();
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
ds.query(options);
@@ -38,6 +49,34 @@ describe('LokiDatasource', () => {
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
});
test('should return log streams when resultFormat is undefined', async done => {
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: 'foo', refId: 'B' }],
});
const res = await ds.query(options);
expect(res.data[0].entries[0].line).toBe('hello');
done();
});
test('should return time series when resultFormat is time_series', async done => {
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
const options = getQueryOptions<LokiQuery>({
targets: [{ expr: 'foo', refId: 'B', resultFormat: 'time_series' }],
});
const res = await ds.query(options);
expect(res.data[0].datapoints).toBeDefined();
done();
});
});
describe('when performing testDataSource', () => {
@@ -32,7 +32,7 @@ function serializeParams(data: any) {
.join('&');
}
export default class LokiDatasource {
export class LokiDatasource {
languageProvider: LanguageProvider;
maxLines: number;
@@ -73,10 +73,11 @@ export default class LokiDatasource {
};
}
query(options: DataQueryOptions<LokiQuery>): Promise<{ data: LogsStream[] }> {
async query(options: DataQueryOptions<LokiQuery>) {
const queryTargets = options.targets
.filter(target => target.expr)
.filter(target => target.expr && !target.hide)
.map(target => this.prepareQueryTarget(target, options));
if (queryTargets.length === 0) {
return Promise.resolve({ data: [] });
}
@@ -84,20 +85,29 @@ export default class LokiDatasource {
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
return Promise.all(queries).then((results: any[]) => {
// Flatten streams from multiple queries
const allStreams: LogsStream[] = results.reduce((acc, response, i) => {
if (!response) {
return acc;
const allStreams: LogsStream[] = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
const query = queryTargets[i];
// add search term to stream & add to array
if (result.data) {
for (const stream of (result.data.streams || [])) {
stream.search = query.regexp;
allStreams.push(stream);
}
}
const streams: LogsStream[] = response.data.streams || [];
// Inject search for match highlighting
const search: string = queryTargets[i].regexp;
streams.forEach(s => {
s.search = search;
});
return [...acc, ...streams];
}, []);
return { data: allStreams };
}
// check resultType
if (options.targets[0].resultFormat === 'time_series') {
const logs = mergeStreamsToLogs(allStreams, this.maxLines);
logs.series = makeSeriesForLogs(logs.rows, options.intervalMs);
return { data: logs.series };
} else {
return { data: allStreams };
}
});
}
@@ -142,34 +152,36 @@ export default class LokiDatasource {
testDatasource() {
return this._request('/api/prom/label')
.then(res => {
if (res && res.data && res.data.values && res.data.values.length > 0) {
return { status: 'success', message: 'Data source connected and labels found.' };
}
return {
status: 'error',
message:
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
};
})
.catch(err => {
let message = 'Loki: ';
if (err.statusText) {
message += err.statusText;
} else {
message += 'Cannot connect to Loki';
}
.then(res => {
if (res && res.data && res.data.values && res.data.values.length > 0) {
return { status: 'success', message: 'Data source connected and labels found.' };
}
return {
status: 'error',
message:
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
};
})
.catch(err => {
let message = 'Loki: ';
if (err.statusText) {
message += err.statusText;
} else {
message += 'Cannot connect to Loki';
}
if (err.status) {
message += `. ${err.status}`;
}
if (err.status) {
message += `. ${err.status}`;
}
if (err.data && err.data.message) {
message += `. ${err.data.message}`;
} else if (err.data) {
message += `. ${err.data}`;
}
return { status: 'error', message: message };
});
if (err.data && err.data.message) {
message += `. ${err.data.message}`;
} else if (err.data) {
message += `. ${err.data}`;
}
return { status: 'error', message: message };
});
}
}
export default LokiDatasource;
@@ -2,6 +2,7 @@ import Datasource from './datasource';
import LokiStartPage from './components/LokiStartPage';
import LokiQueryField from './components/LokiQueryField';
import LokiQueryEditor from './components/LokiQueryEditor';
export class LokiConfigCtrl {
static templateUrl = 'partials/config.html';
@@ -9,6 +10,7 @@ export class LokiConfigCtrl {
export {
Datasource,
LokiQueryEditor as QueryEditor,
LokiConfigCtrl as ConfigCtrl,
LokiQueryField as ExploreQueryField,
LokiStartPage as ExploreStartPage,
@@ -2,12 +2,14 @@
"type": "datasource",
"name": "Loki",
"id": "loki",
"metrics": false,
"metrics": true,
"alerting": false,
"annotations": false,
"logs": true,
"explore": true,
"tables": false,
"info": {
"description": "Loki Logging Data Source for Grafana",
"author": {
@@ -2,5 +2,8 @@ import { DataQuery } from '@grafana/ui/src/types';
export interface LokiQuery extends DataQuery {
expr: string;
resultFormat?: LokiQueryResultFormats;
}
export type LokiQueryResultFormats = 'time_series' | 'logs';
@@ -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) {
Binary file not shown.

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 {
+2 -2
View File
@@ -41,9 +41,9 @@ export class QueryEditor extends PureComponent<Props> {
}
onScenarioChange = (item: SelectOptionItem) => {
this.props.onQueryChange({
this.props.onChange({
...this.props.query,
scenarioId: item.value,
...this.props.query
});
}
+19 -8
View File
@@ -9,28 +9,39 @@ import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
interface Props extends PanelProps<GaugeOptions> {}
export class GaugePanel extends PureComponent<Props> {
render() {
const { timeSeries, width, height, onInterpolate, options } = this.props;
const { panelData, width, height, onInterpolate, options } = this.props;
const prefix = onInterpolate(options.prefix);
const suffix = onInterpolate(options.suffix);
let value: TimeSeriesValue;
const vmSeries = processTimeSeries({
timeSeries: timeSeries,
nullValueMode: NullValueMode.Null,
});
if (panelData.timeSeries) {
const vmSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
nullValueMode: NullValueMode.Null,
});
if (vmSeries[0]) {
value = vmSeries[0].stats[options.stat];
} else {
value = null;
}
} else if (panelData.tableData) {
value = panelData.tableData.rows[0].find(prop => prop > 0);
}
return (
<ThemeProvider>
{(theme) => (
{theme => (
<Gauge
timeSeries={vmSeries}
value={value}
{...this.props.options}
width={width}
height={height}
+1 -1
View File
@@ -281,7 +281,7 @@ class GraphCtrl extends MetricsPanelCtrl {
toggleLegend() {
this.panel.legend.show = !this.panel.legend.show;
this.refresh();
this.render();
}
legendValuesOptionChanged() {
@@ -3,7 +3,7 @@
<h5 class="section-heading">Options</h5>
<gf-form-switch class="gf-form"
label="Show" label-class="width-7"
checked="ctrl.panel.legend.show" on-change="ctrl.refresh()">
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="As Table" label-class="width-7"
@@ -16,13 +16,16 @@ interface Props extends PanelProps<Options> {}
export class GraphPanel extends PureComponent<Props> {
render() {
const { timeSeries, timeRange, width, height } = this.props;
const { panelData, timeRange, width, height } = this.props;
const { showLines, showBars, showPoints } = this.props.options;
const vmSeries = processTimeSeries({
timeSeries: timeSeries,
nullValueMode: NullValueMode.Ignore,
});
let vmSeries;
if (panelData.timeSeries) {
vmSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
nullValueMode: NullValueMode.Ignore,
});
}
return (
<Graph
+22
View File
@@ -280,6 +280,28 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
popover.hide();
}
// hide time picker
const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0;
if (timePickerDropDownIsOpen) {
const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0;
const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0;
const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0;
const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0;
if (
targetIsInTimePickerNav ||
targetIsInTimePickerDropDown ||
targetIsDatePickerRowBtn ||
targetIsDatePickerHeaderBtn
) {
return;
}
scope.$apply(() => {
scope.appEvent('closeTimepicker');
});
}
});
},
};
+2 -2
View File
@@ -1,6 +1,6 @@
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
// import { createLogger } from 'redux-logger';
import { createLogger } from 'redux-logger';
import sharedReducers from 'app/core/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/state/reducers';
@@ -39,7 +39,7 @@ export function configureStore() {
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
} else {
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
}
+26 -11
View File
@@ -1,5 +1,14 @@
import { ComponentClass } from 'react';
import { Value } from 'slate';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryHint } from '@grafana/ui';
import {
RawTimeRange,
TimeRange,
DataQuery,
DataSourceSelectItem,
DataSourceApi,
QueryHint,
ExploreStartPageProps,
} from '@grafana/ui';
import { Emitter } from 'app/core/core';
import { LogsModel } from 'app/core/logs_model';
@@ -102,7 +111,7 @@ export interface ExploreItemState {
/**
* React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet.
*/
StartPage?: any;
StartPage?: ComponentClass<ExploreStartPageProps>;
/**
* Width used for calculating the graph interval (can't have more datapoints than pixels)
*/
@@ -144,10 +153,10 @@ export interface ExploreItemState {
*/
history: HistoryItem[];
/**
* Initial queries for this Explore, e.g., set via URL. Each query will be
* converted to a query row. Query edits should be tracked in `modifiedQueries` though.
* Queries for this Explore, e.g., set via URL. Each query will be
* converted to a query row.
*/
initialQueries: DataQuery[];
queries: DataQuery[];
/**
* True if this Explore area has been initialized.
* Used to distinguish URL state injection versus split view state injection.
@@ -162,12 +171,6 @@ export interface ExploreItemState {
* Log query result to be displayed in the logs result viewer.
*/
logsResult?: LogsModel;
/**
* Copy of `initialQueries` that tracks user edits.
* Don't connect this property to a react component as it is updated on every query change.
* Used when running queries. Needs to be reset to `initialQueries` when those are reset as well.
*/
modifiedQueries: DataQuery[];
/**
* Query intervals for graph queries to determine how many datapoints to return.
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
@@ -229,12 +232,24 @@ export interface ExploreItemState {
* Table model that combines all query table results into a single table.
*/
tableResult?: TableModel;
/**
* React keys for rendering of QueryRows
*/
queryKeys: string[];
}
export interface ExploreUIState {
showingTable: boolean;
showingGraph: boolean;
showingLogs: boolean;
}
export interface ExploreUrlState {
datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
range: RawTimeRange;
ui: ExploreUIState;
}
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
@@ -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

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