Merge branch 'master' into azure-monitor-refactor-#15087
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
client:
|
||||
url: http://loki:3100/api/prom/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: system
|
||||
entry_parser: raw
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: varlogs
|
||||
__path__: /var/log/*log
|
||||
- job_name: grafana
|
||||
entry_parser: raw
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: grafana
|
||||
__path__: /var/log/grafana/*log
|
||||
@@ -1,22 +1,14 @@
|
||||
version: "3"
|
||||
|
||||
networks:
|
||||
loki:
|
||||
|
||||
services:
|
||||
loki:
|
||||
image: grafana/loki:master
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
networks:
|
||||
- loki
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:master
|
||||
volumes:
|
||||
- ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml
|
||||
- /var/log:/var/log
|
||||
- ../data/log:/var/log/grafana
|
||||
command:
|
||||
-config.file=/etc/promtail/docker-config.yaml
|
||||
networks:
|
||||
- loki
|
||||
|
||||
@@ -74,6 +74,12 @@ Here is a minimal policy example:
|
||||
"ec2:DescribeRegions"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "AllowReadingResourcesForTags",
|
||||
"Effect" : "Allow",
|
||||
"Action" : "tag:GetResources",
|
||||
"Resource" : "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -128,6 +134,7 @@ Name | Description
|
||||
*dimension_values(region, namespace, metric, dimension_key, [filters])* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric`, `dimension_key` or you can use dimension `filters` to get more specific result as well.
|
||||
*ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`.
|
||||
*ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attributes matching the specified `region`, `attribute_name`, `filters`.
|
||||
*resource_arns(region, resource_type, tags)* | Returns a list of ARNs matching the specified `region`, `resource_type` and `tags`.
|
||||
|
||||
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
|
||||
|
||||
@@ -143,6 +150,8 @@ Query | Service
|
||||
*dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
|
||||
*dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3
|
||||
*dimension_values(us-east-1,CWAgent,disk_used_percent,device,{"InstanceId":"$instance_id"})* | CloudWatch Agent
|
||||
*resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | ELB
|
||||
*resource_arns(eu-west-1,ec2:instance,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | EC2
|
||||
|
||||
## ec2_instance_attribute examples
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+29
-47
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -136,7 +136,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Logout(c *m.ReqContext) {
|
||||
hs.AuthTokenService.UserSignedOutHook(c)
|
||||
hs.AuthTokenService.SignOutUser(c)
|
||||
|
||||
if setting.SignoutRedirectUrl != "" {
|
||||
c.Redirect(setting.SignoutRedirectUrl)
|
||||
|
||||
@@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
|
||||
func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil }
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -31,7 +32,7 @@ var (
|
||||
type UserAuthTokenService interface {
|
||||
InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
|
||||
UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
|
||||
UserSignedOutHook(c *models.ReqContext)
|
||||
SignOutUser(c *models.ReqContext) error
|
||||
}
|
||||
|
||||
type UserAuthTokenServiceImpl struct {
|
||||
@@ -85,7 +86,7 @@ func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext,
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
|
||||
if setting.Env == setting.DEV {
|
||||
ctx.Logger.Info("new token", "unhashed token", value)
|
||||
ctx.Logger.Debug("new token", "unhashed token", value)
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Del("Set-Cookie")
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@@ -46,6 +51,40 @@ func TestUserAuthToken(t *testing.T) {
|
||||
So(err, ShouldEqual, ErrAuthTokenNotFound)
|
||||
So(LookupToken, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("signing out should delete token and cookie if present", func() {
|
||||
httpreq := &http.Request{Header: make(http.Header)}
|
||||
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken})
|
||||
|
||||
ctx := &models.ReqContext{Context: &macaron.Context{
|
||||
Req: macaron.Request{Request: httpreq},
|
||||
Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
|
||||
},
|
||||
Logger: log.New("fakelogger"),
|
||||
}
|
||||
|
||||
err = userAuthTokenService.SignOutUser(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// makes sure we tell the browser to overwrite the cookie
|
||||
cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName)
|
||||
So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader)
|
||||
})
|
||||
|
||||
Convey("signing out an none existing session should return an error", func() {
|
||||
httpreq := &http.Request{Header: make(http.Header)}
|
||||
httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""})
|
||||
|
||||
ctx := &models.ReqContext{Context: &macaron.Context{
|
||||
Req: macaron.Request{Request: httpreq},
|
||||
Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()),
|
||||
},
|
||||
Logger: log.New("fakelogger"),
|
||||
}
|
||||
|
||||
err = userAuthTokenService.SignOutUser(ctx)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("expires correctly", func() {
|
||||
|
||||
@@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
|
||||
|
||||
cnnstr += ss.buildExtraConnectionString('&')
|
||||
case migrator.POSTGRES:
|
||||
host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432")
|
||||
if ss.dbCfg.Pwd == "" {
|
||||
ss.dbCfg.Pwd = "''"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
@@ -95,10 +96,11 @@ func init() {
|
||||
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
|
||||
"AWS/ML": {"PredictCount", "PredictFailureCount"},
|
||||
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
|
||||
"AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"},
|
||||
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
|
||||
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
|
||||
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
||||
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
|
||||
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
|
||||
@@ -149,6 +151,7 @@ func init() {
|
||||
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
|
||||
"AWS/ML": {"MLModelId", "RequestMode"},
|
||||
"AWS/NATGateway": {"NatGatewayId"},
|
||||
"AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"},
|
||||
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
|
||||
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
||||
"AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
|
||||
@@ -198,6 +201,8 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
|
||||
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
|
||||
case "ec2_instance_attribute":
|
||||
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
|
||||
case "resource_arns":
|
||||
data, err = e.handleGetResourceArns(ctx, parameters, queryContext)
|
||||
}
|
||||
|
||||
transformToTable(data, queryResult)
|
||||
@@ -534,6 +539,65 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) ensureRGTAClientSession(region string) error {
|
||||
if e.rgtaSvc == nil {
|
||||
dsInfo := e.getDsInfo(region)
|
||||
cfg, err := e.getAwsConfig(dsInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to call ec2:NewSession, %v", err)
|
||||
}
|
||||
e.rgtaSvc = resourcegroupstaggingapi.New(sess, cfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleGetResourceArns(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
region := parameters.Get("region").MustString()
|
||||
resourceType := parameters.Get("resourceType").MustString()
|
||||
filterJson := parameters.Get("tags").MustMap()
|
||||
|
||||
err := e.ensureRGTAClientSession(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filters []*resourcegroupstaggingapi.TagFilter
|
||||
for k, v := range filterJson {
|
||||
if vv, ok := v.([]interface{}); ok {
|
||||
var vvvvv []*string
|
||||
for _, vvv := range vv {
|
||||
if vvvv, ok := vvv.(string); ok {
|
||||
vvvvv = append(vvvvv, &vvvv)
|
||||
}
|
||||
}
|
||||
filters = append(filters, &resourcegroupstaggingapi.TagFilter{
|
||||
Key: aws.String(k),
|
||||
Values: vvvvv,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var resourceTypes []*string
|
||||
resourceTypes = append(resourceTypes, &resourceType)
|
||||
|
||||
resources, err := e.resourceGroupsGetResources(region, filters, resourceTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]suggestData, 0)
|
||||
for _, resource := range resources.ResourceTagMappingList {
|
||||
data := *resource.ResourceARN
|
||||
result = append(result, suggestData{Text: data, Value: data})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
|
||||
svc, err := e.getClient(region)
|
||||
if err != nil {
|
||||
@@ -585,6 +649,28 @@ func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) resourceGroupsGetResources(region string, filters []*resourcegroupstaggingapi.TagFilter, resourceTypes []*string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
||||
params := &resourcegroupstaggingapi.GetResourcesInput{
|
||||
ResourceTypeFilters: resourceTypes,
|
||||
TagFilters: filters,
|
||||
}
|
||||
|
||||
var resp resourcegroupstaggingapi.GetResourcesOutput
|
||||
err := e.rgtaSvc.GetResourcesPages(params,
|
||||
func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
|
||||
resources, _ := awsutil.ValuesAtPath(page, "ResourceTagMappingList")
|
||||
for _, resource := range resources {
|
||||
resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, resource.(*resourcegroupstaggingapi.ResourceTagMapping))
|
||||
}
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call tags:GetResources")
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||
creds, err := GetCredentials(cwData)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -22,6 +24,11 @@ type mockedEc2 struct {
|
||||
RespRegions ec2.DescribeRegionsOutput
|
||||
}
|
||||
|
||||
type mockedRGTA struct {
|
||||
resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
|
||||
Resp resourcegroupstaggingapi.GetResourcesOutput
|
||||
}
|
||||
|
||||
func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
|
||||
fn(&m.Resp, true)
|
||||
return nil
|
||||
@@ -30,6 +37,11 @@ func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeR
|
||||
return &m.RespRegions, nil
|
||||
}
|
||||
|
||||
func (m mockedRGTA) GetResourcesPages(in *resourcegroupstaggingapi.GetResourcesInput, fn func(*resourcegroupstaggingapi.GetResourcesOutput, bool) bool) error {
|
||||
fn(&m.Resp, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCloudWatchMetrics(t *testing.T) {
|
||||
|
||||
Convey("When calling getMetricsForCustomMetrics", t, func() {
|
||||
@@ -209,6 +221,51 @@ func TestCloudWatchMetrics(t *testing.T) {
|
||||
So(result[7].Text, ShouldEqual, "vol-4-2")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When calling handleGetResourceArns", t, func() {
|
||||
executor := &CloudWatchExecutor{
|
||||
rgtaSvc: mockedRGTA{
|
||||
Resp: resourcegroupstaggingapi.GetResourcesOutput{
|
||||
ResourceTagMappingList: []*resourcegroupstaggingapi.ResourceTagMapping{
|
||||
{
|
||||
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567"),
|
||||
Tags: []*resourcegroupstaggingapi.Tag{
|
||||
{
|
||||
Key: aws.String("Environment"),
|
||||
Value: aws.String("production"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321"),
|
||||
Tags: []*resourcegroupstaggingapi.Tag{
|
||||
{
|
||||
Key: aws.String("Environment"),
|
||||
Value: aws.String("production"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
json := simplejson.New()
|
||||
json.Set("region", "us-east-1")
|
||||
json.Set("resourceType", "ec2:instance")
|
||||
tags := make(map[string]interface{})
|
||||
tags["Environment"] = []string{"production"}
|
||||
json.Set("tags", tags)
|
||||
result, _ := executor.handleGetResourceArns(context.Background(), json, &tsdb.TsdbQuery{})
|
||||
|
||||
Convey("Should return all two instances", func() {
|
||||
So(result[0].Text, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567")
|
||||
So(result[0].Value, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567")
|
||||
So(result[1].Text, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321")
|
||||
So(result[1].Value, ShouldEqual, "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321")
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseMultiSelectValue(t *testing.T) {
|
||||
|
||||
@@ -49,10 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
server, port, err := util.SplitIPPort(datasource.Url, "1433")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
|
||||
|
||||
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
|
||||
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// SplitIPPort splits the ip string and port.
|
||||
func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) {
|
||||
ipAddr := net.ParseIP(ipStr)
|
||||
|
||||
if ipAddr == nil {
|
||||
// Port was included
|
||||
ip, port, err = net.SplitHostPort(ipStr)
|
||||
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
} else {
|
||||
// No port was included
|
||||
ip = ipAddr.String()
|
||||
port = portDefault
|
||||
}
|
||||
|
||||
return ip, port, nil
|
||||
}
|
||||
+36
-11
@@ -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, "", "")
|
||||
}
|
||||
|
||||
@@ -9,8 +9,90 @@ import (
|
||||
func TestParseIPAddress(t *testing.T) {
|
||||
Convey("Test parse ip address", t, func() {
|
||||
So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
|
||||
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
|
||||
So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
|
||||
So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
|
||||
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
|
||||
So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1")
|
||||
So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSplitHostPortDefault(t *testing.T) {
|
||||
Convey("Test split ip address to host and port", t, func() {
|
||||
host, port := SplitHostPortDefault("192.168.0.140:456", "", "")
|
||||
So(host, ShouldEqual, "192.168.0.140")
|
||||
So(port, ShouldEqual, "456")
|
||||
|
||||
host, port = SplitHostPortDefault("192.168.0.140", "", "123")
|
||||
So(host, ShouldEqual, "192.168.0.140")
|
||||
So(port, ShouldEqual, "123")
|
||||
|
||||
host, port = SplitHostPortDefault("[::1:456]", "", "")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "456")
|
||||
|
||||
host, port = SplitHostPortDefault("[::1]", "", "123")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "123")
|
||||
|
||||
host, port = SplitHostPortDefault("::1:123", "", "")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "123")
|
||||
|
||||
host, port = SplitHostPortDefault("::1", "", "123")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "123")
|
||||
|
||||
host, port = SplitHostPortDefault(":456", "1.2.3.4", "")
|
||||
So(host, ShouldEqual, "1.2.3.4")
|
||||
So(port, ShouldEqual, "456")
|
||||
|
||||
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123")
|
||||
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||
So(port, ShouldEqual, "123")
|
||||
|
||||
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "")
|
||||
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||
So(port, ShouldEqual, "123")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSplitHostPort(t *testing.T) {
|
||||
Convey("Test split ip address to host and port", t, func() {
|
||||
host, port := SplitHostPort("192.168.0.140:456")
|
||||
So(host, ShouldEqual, "192.168.0.140")
|
||||
So(port, ShouldEqual, "456")
|
||||
|
||||
host, port = SplitHostPort("192.168.0.140")
|
||||
So(host, ShouldEqual, "192.168.0.140")
|
||||
So(port, ShouldEqual, "")
|
||||
|
||||
host, port = SplitHostPort("[::1:456]")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "456")
|
||||
|
||||
host, port = SplitHostPort("[::1]")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "")
|
||||
|
||||
host, port = SplitHostPort("::1:123")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "123")
|
||||
|
||||
host, port = SplitHostPort("::1")
|
||||
So(host, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "")
|
||||
|
||||
host, port = SplitHostPort(":456")
|
||||
So(host, ShouldEqual, "")
|
||||
So(port, ShouldEqual, "456")
|
||||
|
||||
host, port = SplitHostPort("xyz.rds.amazonaws.com")
|
||||
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||
So(port, ShouldEqual, "")
|
||||
|
||||
host, port = SplitHostPort("xyz.rds.amazonaws.com:123")
|
||||
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||
So(port, ShouldEqual, "123")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSplitIPPort(t *testing.T) {
|
||||
|
||||
Convey("When parsing an IPv4 without explicit port", t, func() {
|
||||
ip, port, err := SplitIPPort("1.2.3.4", "5678")
|
||||
|
||||
So(err, ShouldEqual, nil)
|
||||
So(ip, ShouldEqual, "1.2.3.4")
|
||||
So(port, ShouldEqual, "5678")
|
||||
})
|
||||
|
||||
Convey("When parsing an IPv6 without explicit port", t, func() {
|
||||
ip, port, err := SplitIPPort("::1", "5678")
|
||||
|
||||
So(err, ShouldEqual, nil)
|
||||
So(ip, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "5678")
|
||||
})
|
||||
|
||||
Convey("When parsing an IPv4 with explicit port", t, func() {
|
||||
ip, port, err := SplitIPPort("1.2.3.4:56", "78")
|
||||
|
||||
So(err, ShouldEqual, nil)
|
||||
So(ip, ShouldEqual, "1.2.3.4")
|
||||
So(port, ShouldEqual, "56")
|
||||
})
|
||||
|
||||
Convey("When parsing an IPv6 with explicit port", t, func() {
|
||||
ip, port, err := SplitIPPort("[::1]:56", "78")
|
||||
|
||||
So(err, ShouldEqual, nil)
|
||||
So(ip, ShouldEqual, "::1")
|
||||
So(port, ShouldEqual, "56")
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,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 {
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||
datasource: null,
|
||||
queries: [],
|
||||
range: DEFAULT_RANGE,
|
||||
ui: {
|
||||
showingGraph: true,
|
||||
showingTable: true,
|
||||
showingLogs: true,
|
||||
}
|
||||
};
|
||||
|
||||
describe('state functions', () => {
|
||||
@@ -69,9 +74,11 @@ describe('state functions', () => {
|
||||
to: 'now',
|
||||
},
|
||||
};
|
||||
|
||||
expect(serializeStateToUrlParam(state)).toBe(
|
||||
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
||||
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
|
||||
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
|
||||
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -93,7 +100,7 @@ describe('state functions', () => {
|
||||
},
|
||||
};
|
||||
expect(serializeStateToUrlParam(state, true)).toBe(
|
||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -118,7 +125,28 @@ describe('state functions', () => {
|
||||
};
|
||||
const serialized = serializeStateToUrlParam(state);
|
||||
const parsed = parseUrlState(serialized);
|
||||
expect(state).toMatchObject(parsed);
|
||||
});
|
||||
|
||||
it('can parse the compact serialized state into the original state', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
datasource: 'foo',
|
||||
queries: [
|
||||
{
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
range: {
|
||||
from: 'now - 5h',
|
||||
to: 'now',
|
||||
},
|
||||
};
|
||||
const serialized = serializeStateToUrlParam(state, true);
|
||||
const parsed = parseUrlState(serialized);
|
||||
expect(state).toMatchObject(parsed);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { colors } from '@grafana/ui';
|
||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||
|
||||
// Types
|
||||
import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types';
|
||||
import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui/src/types';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import {
|
||||
ExploreUrlState,
|
||||
@@ -27,6 +27,12 @@ export const DEFAULT_RANGE = {
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
export const DEFAULT_UI_STATE = {
|
||||
showingTable: true,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
};
|
||||
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
|
||||
@@ -147,7 +153,12 @@ export function buildQueryTransaction(
|
||||
|
||||
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||
|
||||
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
|
||||
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
|
||||
|
||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
let uiState = DEFAULT_UI_STATE;
|
||||
|
||||
if (initial) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURI(initial));
|
||||
@@ -160,20 +171,41 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
to: parsed[1],
|
||||
};
|
||||
const datasource = parsed[2];
|
||||
const queries = parsed.slice(3);
|
||||
return { datasource, queries, range };
|
||||
let queries = [];
|
||||
|
||||
parsed.slice(3).forEach(segment => {
|
||||
if (isMetricSegment(segment)) {
|
||||
queries = [...queries, segment];
|
||||
}
|
||||
|
||||
if (isUISegment(segment)) {
|
||||
uiState = {
|
||||
showingGraph: segment.ui[0],
|
||||
showingLogs: segment.ui[1],
|
||||
showingTable: segment.ui[2],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return { datasource, queries, range, ui: uiState };
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
||||
return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: uiState };
|
||||
}
|
||||
|
||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||
if (compact) {
|
||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
||||
return JSON.stringify([
|
||||
urlState.range.from,
|
||||
urlState.range.to,
|
||||
urlState.datasource,
|
||||
...urlState.queries,
|
||||
{ ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] },
|
||||
]);
|
||||
}
|
||||
return JSON.stringify(urlState);
|
||||
}
|
||||
@@ -304,3 +336,12 @@ export function clearHistory(datasourceId: string) {
|
||||
const historyKey = `grafana.explore.history.${datasourceId}`;
|
||||
store.delete(historyKey);
|
||||
}
|
||||
|
||||
export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => {
|
||||
const queryKeys = queries.reduce((newQueryKeys, query, index) => {
|
||||
const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
|
||||
return newQueryKeys.concat(`${primaryKey}-${index}`);
|
||||
}, []);
|
||||
|
||||
return queryKeys;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AddPanelWidget, Props } from './AddPanelWidget';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
dashboard: {} as DashboardModel,
|
||||
panel: {} as PanelModel,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<AddPanelWidget {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,20 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import store from 'app/core/store';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Store
|
||||
import { store as reduxStore } from 'app/store/store';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../../state';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { LocationUpdate } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -46,6 +54,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
copiedPanels.push(pluginCopy);
|
||||
}
|
||||
}
|
||||
|
||||
return _.sortBy(copiedPanels, 'sort');
|
||||
}
|
||||
|
||||
@@ -54,28 +63,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
|
||||
}
|
||||
|
||||
copyButton(panel) {
|
||||
return (
|
||||
<button className="btn-inverse btn" onClick={() => this.onPasteCopiedPanel(panel)} title={panel.name}>
|
||||
Paste copied Panel
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
moveToEdit(panel) {
|
||||
reduxStore.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: panel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onCreateNewPanel = () => {
|
||||
onCreateNewPanel = (tab = 'queries') => {
|
||||
const dashboard = this.props.dashboard;
|
||||
const { gridPos } = this.props.panel;
|
||||
|
||||
@@ -88,7 +76,21 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
dashboard.addPanel(newPanel);
|
||||
dashboard.removePanel(this.props.panel);
|
||||
|
||||
this.moveToEdit(newPanel);
|
||||
const location: LocationUpdate = {
|
||||
query: {
|
||||
panelId: newPanel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
partial: true,
|
||||
};
|
||||
|
||||
if (tab === 'visualization') {
|
||||
location.query.tab = 'visualization';
|
||||
location.query.openVizPicker = true;
|
||||
}
|
||||
|
||||
reduxStore.dispatch(updateLocation(location));
|
||||
};
|
||||
|
||||
onPasteCopiedPanel = panelPluginInfo => {
|
||||
@@ -125,30 +127,50 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
dashboard.removePanel(this.props.panel);
|
||||
};
|
||||
|
||||
render() {
|
||||
let addCopyButton;
|
||||
renderOptionLink = (icon, text, onClick) => {
|
||||
return (
|
||||
<div>
|
||||
<a href="#" onClick={onClick} className="add-panel-widget__link btn btn-inverse">
|
||||
<div className="add-panel-widget__icon">
|
||||
<i className={`gicon gicon-${icon}`} />
|
||||
</div>
|
||||
<span>{text}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (this.state.copiedPanelPlugins.length === 1) {
|
||||
addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]);
|
||||
}
|
||||
render() {
|
||||
const { copiedPanelPlugins } = this.state;
|
||||
|
||||
return (
|
||||
<div className="panel-container add-panel-widget-container">
|
||||
<div className="add-panel-widget">
|
||||
<div className="add-panel-widget__header grid-drag-handle">
|
||||
<i className="gicon gicon-add-panel" />
|
||||
<span className="add-panel-widget__title">New Panel</span>
|
||||
<button className="add-panel-widget__close" onClick={this.handleCloseAddPanel}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="add-panel-widget__btn-container">
|
||||
<button className="btn-success btn btn-large" onClick={this.onCreateNewPanel}>
|
||||
Edit Panel
|
||||
</button>
|
||||
{addCopyButton}
|
||||
<button className="btn-inverse btn" onClick={this.onCreateNewRow}>
|
||||
Add Row
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface QueryEditorProps {
|
||||
datasource: any;
|
||||
error?: string | JSX.Element;
|
||||
onExecuteQuery?: () => void;
|
||||
onQueryChange?: (value: DataQuery, override?: boolean) => void;
|
||||
onQueryChange?: (value: DataQuery) => void;
|
||||
initialQuery: DataQuery;
|
||||
exploreEvents: Emitter;
|
||||
range: RawTimeRange;
|
||||
@@ -40,20 +40,17 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
||||
datasource,
|
||||
target,
|
||||
refresh: () => {
|
||||
this.props.onQueryChange(target, false);
|
||||
this.props.onQueryChange(target);
|
||||
this.props.onExecuteQuery();
|
||||
},
|
||||
events: exploreEvents,
|
||||
panel: {
|
||||
datasource,
|
||||
targets: [target],
|
||||
},
|
||||
panel: { datasource, targets: [target] },
|
||||
dashboard: {},
|
||||
},
|
||||
};
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
this.props.onQueryChange(target, false);
|
||||
this.props.onQueryChange(target);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@@ -33,10 +33,9 @@ export interface QueryFieldProps {
|
||||
cleanText?: (text: string) => string;
|
||||
disabled?: boolean;
|
||||
initialQuery: string | null;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
onExecuteQuery?: () => void;
|
||||
onQueryChange?: (value: string) => void;
|
||||
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
|
||||
onValueChanged?: (value: string) => void;
|
||||
onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
|
||||
placeholder?: string;
|
||||
portalOrigin?: string;
|
||||
@@ -51,6 +50,7 @@ export interface QueryFieldState {
|
||||
typeaheadPrefix: string;
|
||||
typeaheadText: string;
|
||||
value: Value;
|
||||
lastExecutedValue: Value;
|
||||
}
|
||||
|
||||
export interface TypeaheadInput {
|
||||
@@ -90,6 +90,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
typeaheadPrefix: '',
|
||||
typeaheadText: '',
|
||||
value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
|
||||
lastExecutedValue: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,25 +6,23 @@ import QueryRow from './QueryRow';
|
||||
|
||||
// Types
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
interface QueryRowsProps {
|
||||
className?: string;
|
||||
exploreEvents: Emitter;
|
||||
exploreId: ExploreId;
|
||||
initialQueries: DataQuery[];
|
||||
queryKeys: string[];
|
||||
}
|
||||
|
||||
export default class QueryRows extends PureComponent<QueryRowsProps> {
|
||||
render() {
|
||||
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
|
||||
const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
|
||||
return (
|
||||
<div className={className}>
|
||||
{initialQueries.map((query, index) => (
|
||||
// TODO instead of relying on initialQueries, move to react key list in redux
|
||||
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
|
||||
))}
|
||||
{queryKeys.map((key, index) => {
|
||||
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ import { StoreState } from 'app/types';
|
||||
import { ExploreId, ExploreUrlState } from 'app/types/explore';
|
||||
import { parseUrlState } from 'app/core/utils/explore';
|
||||
|
||||
import { initializeExploreSplit, resetExplore } from './state/actions';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import Explore from './Explore';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
|
||||
|
||||
interface WrapperProps {
|
||||
initializeExploreSplit: typeof initializeExploreSplit;
|
||||
initializeExploreSplitAction: typeof initializeExploreSplitAction;
|
||||
split: boolean;
|
||||
updateLocation: typeof updateLocation;
|
||||
resetExplore: typeof resetExplore;
|
||||
resetExploreAction: typeof resetExploreAction;
|
||||
urlStates: { [key: string]: string };
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ export class Wrapper extends Component<WrapperProps> {
|
||||
|
||||
componentDidMount() {
|
||||
if (this.initialSplit) {
|
||||
this.props.initializeExploreSplit();
|
||||
this.props.initializeExploreSplitAction();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetExplore();
|
||||
this.props.resetExploreAction();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => {
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initializeExploreSplit,
|
||||
initializeExploreSplitAction,
|
||||
updateLocation,
|
||||
resetExplore,
|
||||
resetExploreAction,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -280,6 +280,28 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
|
||||
popover.hide();
|
||||
}
|
||||
|
||||
// hide time picker
|
||||
const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0;
|
||||
if (timePickerDropDownIsOpen) {
|
||||
const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0;
|
||||
const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0;
|
||||
const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0;
|
||||
const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0;
|
||||
|
||||
if (
|
||||
targetIsInTimePickerNav ||
|
||||
targetIsInTimePickerDropDown ||
|
||||
targetIsDatePickerRowBtn ||
|
||||
targetIsDatePickerHeaderBtn
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
scope.$apply(() => {
|
||||
scope.appEvent('closeTimepicker');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
// import { createLogger } from 'redux-logger';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import sharedReducers from 'app/core/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
import teamsReducers from 'app/features/teams/state/reducers';
|
||||
@@ -39,7 +39,7 @@ export function configureStore() {
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// DEV builds we had the logger middleware
|
||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
|
||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
|
||||
} else {
|
||||
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
|
||||
}
|
||||
|
||||
+26
-11
@@ -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
Reference in New Issue
Block a user