mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into delete_session_on_logout
This commit is contained in:
commit
fb3c510178
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
|
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
|
||||||
|
* **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson)
|
||||||
|
|
||||||
# 6.0.0-beta1 (2019-01-30)
|
# 6.0.0-beta1 (2019-01-30)
|
||||||
|
|
||||||
|
54
README.md
54
README.md
@ -25,49 +25,71 @@ the latest master builds [here](https://grafana.com/grafana/download)
|
|||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Go (Latest Stable)
|
- Go (Latest Stable)
|
||||||
|
- bra [`go get github.com/Unknwon/bra`]
|
||||||
- Node.js LTS
|
- 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
|
```bash
|
||||||
go get github.com/grafana/grafana
|
go get github.com/grafana/grafana
|
||||||
cd $GOPATH/src/github.com/grafana/grafana
|
cd $GOPATH/src/github.com/grafana/grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
#### The backend
|
||||||
|
|
||||||
|
```bash
|
||||||
go run build.go setup
|
go run build.go setup
|
||||||
go run build.go build
|
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
|
```bash
|
||||||
npm install -g yarn
|
|
||||||
yarn install --pure-lockfile
|
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
|
yarn watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
|
Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn start
|
||||||
# OR set a theme
|
# OR set a theme
|
||||||
env GRAFANA_THEME=light yarn start
|
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
|
```bash
|
||||||
yarn jest
|
yarn jest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Recompile backend on source change
|
**Open grafana in your browser (default: e.g. `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).**
|
||||||
|
|
||||||
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`).
|
|
||||||
|
|
||||||
### Building a Docker image
|
### Building a Docker image
|
||||||
|
|
||||||
|
@ -113,6 +113,9 @@ cache_mode = private
|
|||||||
# Login cookie name
|
# Login cookie name
|
||||||
cookie_name = grafana_session
|
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
|
# How many days an session can be unused before we inactivate it
|
||||||
login_remember_days = 7
|
login_remember_days = 7
|
||||||
|
|
||||||
|
@ -109,6 +109,9 @@ log_queries =
|
|||||||
# Login cookie name
|
# Login cookie name
|
||||||
;cookie_name = grafana_session
|
;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
|
# How many days an session can be unused before we inactivate it
|
||||||
;login_remember_days = 7
|
;login_remember_days = 7
|
||||||
|
|
||||||
|
27
devenv/docker/blocks/loki/config.yaml
Normal file
27
devenv/docker/blocks/loki/config.yaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
server:
|
||||||
|
http_listen_port: 9080
|
||||||
|
grpc_listen_port: 0
|
||||||
|
|
||||||
|
positions:
|
||||||
|
filename: /tmp/positions.yaml
|
||||||
|
|
||||||
|
client:
|
||||||
|
url: http://loki:3100/api/prom/push
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: system
|
||||||
|
entry_parser: raw
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost
|
||||||
|
labels:
|
||||||
|
job: varlogs
|
||||||
|
__path__: /var/log/*log
|
||||||
|
- job_name: grafana
|
||||||
|
entry_parser: raw
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost
|
||||||
|
labels:
|
||||||
|
job: grafana
|
||||||
|
__path__: /var/log/grafana/*log
|
@ -1,24 +1,14 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
loki:
|
|
||||||
|
|
||||||
services:
|
|
||||||
loki:
|
loki:
|
||||||
image: grafana/loki:master
|
image: grafana/loki:master
|
||||||
ports:
|
ports:
|
||||||
- "3100:3100"
|
- "3100:3100"
|
||||||
command: -config.file=/etc/loki/local-config.yaml
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
networks:
|
|
||||||
- loki
|
|
||||||
|
|
||||||
promtail:
|
promtail:
|
||||||
image: grafana/promtail:master
|
image: grafana/promtail:master
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml
|
||||||
- /var/log:/var/log
|
- /var/log:/var/log
|
||||||
|
- ../data/log:/var/log/grafana
|
||||||
command:
|
command:
|
||||||
-config.file=/etc/promtail/docker-config.yaml
|
-config.file=/etc/promtail/docker-config.yaml
|
||||||
networks:
|
|
||||||
- loki
|
|
||||||
depends_on:
|
|
||||||
- loki
|
|
||||||
|
@ -167,6 +167,7 @@ $arrowSize: 15px;
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sp-replacer:hover,
|
.sp-replacer:hover,
|
||||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { Gauge, Props } from './Gauge';
|
import { Gauge, Props } from './Gauge';
|
||||||
import { TimeSeriesVMs } from '../../types/series';
|
|
||||||
import { ValueMapping, MappingType } from '../../types';
|
import { ValueMapping, MappingType } from '../../types';
|
||||||
|
|
||||||
jest.mock('jquery', () => ({
|
jest.mock('jquery', () => ({
|
||||||
@ -23,7 +22,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
stat: 'avg',
|
stat: 'avg',
|
||||||
height: 300,
|
height: 300,
|
||||||
width: 300,
|
width: 300,
|
||||||
timeSeries: {} as TimeSeriesVMs,
|
value: 25,
|
||||||
decimals: 0,
|
decimals: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
import { ValueMapping, Threshold, BasicGaugeColor, TimeSeriesVMs, GrafanaTheme } from '../../types';
|
import { ValueMapping, Threshold, BasicGaugeColor, GrafanaTheme } from '../../types';
|
||||||
import { getMappedValue } from '../../utils/valueMappings';
|
import { getMappedValue } from '../../utils/valueMappings';
|
||||||
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
|
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
|
||||||
|
|
||||||
@ -14,7 +14,6 @@ export interface Props {
|
|||||||
maxValue: number;
|
maxValue: number;
|
||||||
minValue: number;
|
minValue: number;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
timeSeries: TimeSeriesVMs;
|
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
showThresholdMarkers: boolean;
|
showThresholdMarkers: boolean;
|
||||||
showThresholdLabels: boolean;
|
showThresholdLabels: boolean;
|
||||||
@ -22,6 +21,7 @@ export interface Props {
|
|||||||
suffix: string;
|
suffix: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
width: number;
|
width: number;
|
||||||
|
value: number;
|
||||||
theme?: GrafanaTheme;
|
theme?: GrafanaTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,25 +122,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
const {
|
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
|
||||||
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 formattedValue = this.formatValue(value) as string;
|
const formattedValue = this.formatValue(value) as string;
|
||||||
const dimension = Math.min(width, height * 1.3);
|
const dimension = Math.min(width, height * 1.3);
|
||||||
@ -194,7 +176,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
try {
|
try {
|
||||||
$.plot(this.canvasElement, [plotSeries], options);
|
$.plot(this.canvasElement, [plotSeries], options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Gauge rendering error', err, options, timeSeries);
|
console.log('Gauge rendering error', err, options, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,26 +1,38 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { SFC } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
onClose?: () => void;
|
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 (
|
return (
|
||||||
<div className="panel-options-group">
|
<div className="panel-options-group">
|
||||||
{props.title && (
|
{props.onAdd ? (
|
||||||
<div className="panel-options-group__header">
|
<div className="panel-options-group__header">
|
||||||
{props.title}
|
<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 && (
|
{props.onClose && (
|
||||||
<button className="btn btn-link" onClick={props.onClose}>
|
<button className="btn btn-link" onClick={props.onClose}>
|
||||||
<i className="fa fa-remove" />
|
<i className="fa fa-remove" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
<div className="panel-options-group__body">{props.children}</div>
|
{props.children && <div className="panel-options-group__body">{props.children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,18 +7,57 @@
|
|||||||
|
|
||||||
.panel-options-group__header {
|
.panel-options-group__header {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 1.1rem;
|
|
||||||
background: $panel-options-group-header-bg;
|
background: $panel-options-group-header-bg;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: $border-radius $border-radius 0 0;
|
border-radius: $border-radius $border-radius 0 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
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 {
|
.panel-options-group__body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
|
|||||||
return (
|
return (
|
||||||
<div className="gf-form-select-box__option-group">
|
<div className="gf-form-select-box__option-group">
|
||||||
<div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
|
<div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
|
||||||
<span className="flex-grow">{label}</span>
|
<span className="flex-grow-1">{label}</span>
|
||||||
<i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
|
<i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
|
||||||
</div>
|
</div>
|
||||||
{expanded && children}
|
{expanded && children}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
.thresholds {
|
.thresholds {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thresholds-row {
|
.thresholds-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 70px;
|
height: 62px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thresholds-row:first-child > .thresholds-row-color-indicator {
|
.thresholds-row:first-child > .thresholds-row-color-indicator {
|
||||||
@ -21,21 +21,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.thresholds-row-add-button {
|
.thresholds-row-add-button {
|
||||||
|
@include buttonBackground($btn-success-bg, $btn-success-bg-hl, $text-color);
|
||||||
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
color: $green;
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
background-color: $green;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-add-button > i {
|
&:hover {
|
||||||
color: $white;
|
color: $text-color-strong;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thresholds-row-color-indicator {
|
.thresholds-row-color-indicator {
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
|
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
|
||||||
import { MappingType } from '../../types/panel';
|
import { MappingType } from '../../types';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import MappingRow from './MappingRow';
|
import MappingRow from './MappingRow';
|
||||||
import { MappingType, ValueMapping } from '../../types/panel';
|
import { MappingType, ValueMapping } from '../../types';
|
||||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
import { PanelOptionsGroup } from '..';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
valueMappings: ValueMapping[];
|
valueMappings: ValueMapping[];
|
||||||
@ -81,8 +81,7 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
|||||||
const { valueMappings } = this.state;
|
const { valueMappings } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelOptionsGroup title="Value Mappings">
|
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
|
||||||
<div>
|
|
||||||
{valueMappings.length > 0 &&
|
{valueMappings.length > 0 &&
|
||||||
valueMappings.map((valueMapping, index) => (
|
valueMappings.map((valueMapping, index) => (
|
||||||
<MappingRow
|
<MappingRow
|
||||||
@ -92,13 +91,6 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
|||||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
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>
|
</PanelOptionsGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
exports[`Render should render component 1`] = `
|
||||||
<Component
|
<Component
|
||||||
title="Value Mappings"
|
onAdd={[Function]}
|
||||||
|
title="Add value mapping"
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<MappingRow
|
<MappingRow
|
||||||
key="Ok-0"
|
key="Ok-0"
|
||||||
removeValueMapping={[Function]}
|
removeValueMapping={[Function]}
|
||||||
@ -34,23 +34,5 @@ exports[`Render should render component 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</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>
|
</Component>
|
||||||
`;
|
`;
|
||||||
|
@ -52,3 +52,20 @@ export interface TimeSeriesVMs {
|
|||||||
[index: number]: TimeSeriesVM;
|
[index: number]: TimeSeriesVM;
|
||||||
length: number;
|
length: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
text: string;
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
sort?: boolean;
|
||||||
|
desc?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableData {
|
||||||
|
columns: Column[];
|
||||||
|
rows: any[];
|
||||||
|
type: string;
|
||||||
|
columnMap: any;
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { TimeRange, RawTimeRange } from './time';
|
import { TimeRange, RawTimeRange } from './time';
|
||||||
import { TimeSeries } from './series';
|
|
||||||
import { PluginMeta } from './plugin';
|
import { PluginMeta } from './plugin';
|
||||||
|
import { TableData, TimeSeries } from './data';
|
||||||
|
|
||||||
export interface DataQueryResponse {
|
export interface DataQueryResponse {
|
||||||
data: TimeSeries[];
|
data: TimeSeries[] | [TableData] | any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataQuery {
|
export interface DataQuery {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from './series';
|
export * from './data';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './panel';
|
export * from './panel';
|
||||||
export * from './plugin';
|
export * from './plugin';
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { TimeSeries, LoadingState } from './series';
|
import { TimeSeries, LoadingState, TableData } from './data';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
|
|
||||||
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
||||||
|
|
||||||
export interface PanelProps<T = any> {
|
export interface PanelProps<T = any> {
|
||||||
timeSeries: TimeSeries[];
|
panelData: PanelData;
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
loading: LoadingState;
|
loading: LoadingState;
|
||||||
options: T;
|
options: T;
|
||||||
@ -14,6 +14,11 @@ export interface PanelProps<T = any> {
|
|||||||
onInterpolate: InterpolateFunction;
|
onInterpolate: InterpolateFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PanelData {
|
||||||
|
timeSeries?: TimeSeries[];
|
||||||
|
tableData?: TableData;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PanelOptionsProps<T = any> {
|
export interface PanelOptionsProps<T = any> {
|
||||||
options: T;
|
options: T;
|
||||||
onChange: (options: T) => void;
|
onChange: (options: T) => void;
|
||||||
|
@ -44,8 +44,8 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
|||||||
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||||
datasource: DSType;
|
datasource: DSType;
|
||||||
query: TQuery;
|
query: TQuery;
|
||||||
onExecuteQuery?: () => void;
|
onRunQuery: () => void;
|
||||||
onQueryChange?: (value: TQuery) => void;
|
onChange: (value: TQuery) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginExports {
|
export interface PluginExports {
|
||||||
|
@ -108,8 +108,8 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
|
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
|
||||||
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
|
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
|
||||||
|
|
||||||
// api renew session based on remember cookie
|
// api renew session based on cookie
|
||||||
r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
|
r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing))
|
||||||
|
|
||||||
// authed api
|
// authed api
|
||||||
r.Group("/api", func(apiRoute routing.RouteRegister) {
|
r.Group("/api", func(apiRoute routing.RouteRegister) {
|
||||||
|
@ -97,6 +97,7 @@ func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, va
|
|||||||
Path: setting.AppSubUrl + "/",
|
Path: setting.AppSubUrl + "/",
|
||||||
Secure: s.Cfg.SecurityHTTPSCookies,
|
Secure: s.Cfg.SecurityHTTPSCookies,
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
|
SameSite: s.Cfg.LoginCookieSameSite,
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(ctx.Resp, &cookie)
|
http.SetCookie(ctx.Resp, &cookie)
|
||||||
@ -163,7 +164,7 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent
|
|||||||
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
|
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
|
||||||
hashedToken := hashToken(unhashedToken)
|
hashedToken := hashToken(unhashedToken)
|
||||||
if setting.Env == setting.DEV {
|
if setting.Env == setting.DEV {
|
||||||
s.log.Info("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
|
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
|
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
|
||||||
|
@ -92,7 +92,7 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Result == nil {
|
if cmd.Result == nil {
|
||||||
dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name, "uid", notification.Uid)
|
dc.log.Debug("inserting alert notification from configuration", "name", notification.Name, "uid", notification.Uid)
|
||||||
insertCmd := &models.CreateAlertNotificationCommand{
|
insertCmd := &models.CreateAlertNotificationCommand{
|
||||||
Uid: notification.Uid,
|
Uid: notification.Uid,
|
||||||
Name: notification.Name,
|
Name: notification.Name,
|
||||||
@ -109,7 +109,7 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
|
dc.log.Debug("updating alert notification from configuration", "name", notification.Name)
|
||||||
updateCmd := &models.UpdateAlertNotificationWithUidCommand{
|
updateCmd := &models.UpdateAlertNotificationWithUidCommand{
|
||||||
Uid: notification.Uid,
|
Uid: notification.Uid,
|
||||||
Name: notification.Name,
|
Name: notification.Name,
|
||||||
|
@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
|
|||||||
|
|
||||||
cnnstr += ss.buildExtraConnectionString('&')
|
cnnstr += ss.buildExtraConnectionString('&')
|
||||||
case migrator.POSTGRES:
|
case migrator.POSTGRES:
|
||||||
host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432")
|
host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432")
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if ss.dbCfg.Pwd == "" {
|
if ss.dbCfg.Pwd == "" {
|
||||||
ss.dbCfg.Pwd = "''"
|
ss.dbCfg.Pwd = "''"
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ package setting
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -227,6 +228,7 @@ type Cfg struct {
|
|||||||
LoginCookieMaxDays int
|
LoginCookieMaxDays int
|
||||||
LoginCookieRotation int
|
LoginCookieRotation int
|
||||||
LoginDeleteExpiredTokensAfterDays int
|
LoginDeleteExpiredTokensAfterDays int
|
||||||
|
LoginCookieSameSite http.SameSite
|
||||||
|
|
||||||
SecurityHTTPSCookies bool
|
SecurityHTTPSCookies bool
|
||||||
}
|
}
|
||||||
@ -557,6 +559,20 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
|
cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
|
||||||
cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
|
cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
|
||||||
cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30)
|
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)
|
cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10)
|
||||||
if cfg.LoginCookieRotation < 2 {
|
if cfg.LoginCookieRotation < 2 {
|
||||||
cfg.LoginCookieRotation = 2
|
cfg.LoginCookieRotation = 2
|
||||||
|
@ -95,6 +95,7 @@ func init() {
|
|||||||
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
|
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
|
||||||
"AWS/ML": {"PredictCount", "PredictFailureCount"},
|
"AWS/ML": {"PredictCount", "PredictFailureCount"},
|
||||||
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
|
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
|
||||||
|
"AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"},
|
||||||
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
|
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
|
||||||
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
|
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
|
||||||
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||||
@ -149,6 +150,7 @@ func init() {
|
|||||||
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
|
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
|
||||||
"AWS/ML": {"MLModelId", "RequestMode"},
|
"AWS/ML": {"MLModelId", "RequestMode"},
|
||||||
"AWS/NATGateway": {"NatGatewayId"},
|
"AWS/NATGateway": {"NatGatewayId"},
|
||||||
|
"AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"},
|
||||||
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
|
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
|
||||||
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
|
||||||
"AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
|
"AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
|
||||||
|
@ -49,10 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server, port, err := util.SplitIPPort(datasource.Url, "1433")
|
server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
|
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
|
||||||
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SplitIPPort splits the ip string and port.
|
|
||||||
func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) {
|
|
||||||
ipAddr := net.ParseIP(ipStr)
|
|
||||||
|
|
||||||
if ipAddr == nil {
|
|
||||||
// Port was included
|
|
||||||
ip, port, err = net.SplitHostPort(ipStr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No port was included
|
|
||||||
ip = ipAddr.String()
|
|
||||||
port = portDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, port, nil
|
|
||||||
}
|
|
@ -7,19 +7,13 @@ import (
|
|||||||
|
|
||||||
// ParseIPAddress parses an IP address and removes port and/or IPV6 format
|
// ParseIPAddress parses an IP address and removes port and/or IPV6 format
|
||||||
func ParseIPAddress(input string) string {
|
func ParseIPAddress(input string) string {
|
||||||
s := input
|
host, _ := SplitHostPort(input)
|
||||||
lastIndex := strings.LastIndex(input, ":")
|
|
||||||
|
|
||||||
if lastIndex != -1 {
|
ip := net.ParseIP(host)
|
||||||
if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
|
|
||||||
s = input[:lastIndex]
|
if ip == nil {
|
||||||
|
return host
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
s = strings.Replace(s, "[", "", -1)
|
|
||||||
s = strings.Replace(s, "]", "", -1)
|
|
||||||
|
|
||||||
ip := net.ParseIP(s)
|
|
||||||
|
|
||||||
if ip.IsLoopback() {
|
if ip.IsLoopback() {
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
@ -27,3 +21,34 @@ func ParseIPAddress(input string) string {
|
|||||||
|
|
||||||
return ip.String()
|
return ip.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SplitHostPortDefault splits ip address/hostname string by host and port. Defaults used if no match found
|
||||||
|
func SplitHostPortDefault(input, defaultHost, defaultPort string) (host string, port string) {
|
||||||
|
port = defaultPort
|
||||||
|
s := input
|
||||||
|
lastIndex := strings.LastIndex(input, ":")
|
||||||
|
|
||||||
|
if lastIndex != -1 {
|
||||||
|
if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
|
||||||
|
s = input[:lastIndex]
|
||||||
|
port = input[lastIndex+1:]
|
||||||
|
} else if lastIndex == 0 {
|
||||||
|
s = defaultHost
|
||||||
|
port = input[lastIndex+1:]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
port = defaultPort
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.Replace(s, "[", "", -1)
|
||||||
|
s = strings.Replace(s, "]", "", -1)
|
||||||
|
port = strings.Replace(port, "[", "", -1)
|
||||||
|
port = strings.Replace(port, "]", "", -1)
|
||||||
|
|
||||||
|
return s, port
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitHostPort splits ip address/hostname string by host and port
|
||||||
|
func SplitHostPort(input string) (host string, port string) {
|
||||||
|
return SplitHostPortDefault(input, "", "")
|
||||||
|
}
|
||||||
|
@ -9,8 +9,90 @@ import (
|
|||||||
func TestParseIPAddress(t *testing.T) {
|
func TestParseIPAddress(t *testing.T) {
|
||||||
Convey("Test parse ip address", t, func() {
|
Convey("Test parse ip address", t, func() {
|
||||||
So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
|
So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
|
||||||
|
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
|
||||||
So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
|
So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
|
||||||
So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
|
So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
|
||||||
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
|
So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1")
|
||||||
|
So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitHostPortDefault(t *testing.T) {
|
||||||
|
Convey("Test split ip address to host and port", t, func() {
|
||||||
|
host, port := SplitHostPortDefault("192.168.0.140:456", "", "")
|
||||||
|
So(host, ShouldEqual, "192.168.0.140")
|
||||||
|
So(port, ShouldEqual, "456")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("192.168.0.140", "", "123")
|
||||||
|
So(host, ShouldEqual, "192.168.0.140")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("[::1:456]", "", "")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "456")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("[::1]", "", "123")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("::1:123", "", "")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("::1", "", "123")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault(":456", "1.2.3.4", "")
|
||||||
|
So(host, ShouldEqual, "1.2.3.4")
|
||||||
|
So(port, ShouldEqual, "456")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123")
|
||||||
|
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
|
||||||
|
host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "")
|
||||||
|
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitHostPort(t *testing.T) {
|
||||||
|
Convey("Test split ip address to host and port", t, func() {
|
||||||
|
host, port := SplitHostPort("192.168.0.140:456")
|
||||||
|
So(host, ShouldEqual, "192.168.0.140")
|
||||||
|
So(port, ShouldEqual, "456")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("192.168.0.140")
|
||||||
|
So(host, ShouldEqual, "192.168.0.140")
|
||||||
|
So(port, ShouldEqual, "")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("[::1:456]")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "456")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("[::1]")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("::1:123")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("::1")
|
||||||
|
So(host, ShouldEqual, "::1")
|
||||||
|
So(port, ShouldEqual, "")
|
||||||
|
|
||||||
|
host, port = SplitHostPort(":456")
|
||||||
|
So(host, ShouldEqual, "")
|
||||||
|
So(port, ShouldEqual, "456")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("xyz.rds.amazonaws.com")
|
||||||
|
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||||
|
So(port, ShouldEqual, "")
|
||||||
|
|
||||||
|
host, port = SplitHostPort("xyz.rds.amazonaws.com:123")
|
||||||
|
So(host, ShouldEqual, "xyz.rds.amazonaws.com")
|
||||||
|
So(port, ShouldEqual, "123")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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 React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Select } from '@grafana/ui';
|
import { Select, SelectOptionItem } from '@grafana/ui';
|
||||||
import { SelectOptionItem } from '@grafana/ui';
|
|
||||||
import { Variable } from 'app/types/templates';
|
import { Variable } from 'app/types/templates';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import _ from 'lodash';
|
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';
|
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 a[1] - b[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
return new TimeSeries(series);
|
return { datapoints: series.datapoints, target: series.alias, color: series.color };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,106 +1,20 @@
|
|||||||
import $ from 'jquery';
|
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
export class Profiler {
|
export class Profiler {
|
||||||
panelsRendered: number;
|
panelsRendered: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
panelsInitCount: any;
|
|
||||||
timings: any;
|
|
||||||
digestCounter: any;
|
|
||||||
$rootScope: any;
|
$rootScope: any;
|
||||||
scopeCount: any;
|
|
||||||
window: any;
|
window: any;
|
||||||
|
|
||||||
init(config, $rootScope) {
|
init(config, $rootScope) {
|
||||||
this.enabled = config.buildInfo.env === 'development';
|
|
||||||
this.timings = {};
|
|
||||||
this.timings.appStart = { loadStart: new Date().getTime() };
|
|
||||||
this.$rootScope = $rootScope;
|
this.$rootScope = $rootScope;
|
||||||
this.window = window;
|
this.window = window;
|
||||||
|
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$rootScope.$watch(
|
|
||||||
() => {
|
|
||||||
this.digestCounter++;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
$rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope);
|
|
||||||
$rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope);
|
|
||||||
$rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this), $rootScope);
|
|
||||||
$rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this), $rootScope);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
renderingCompleted(panelId) {
|
||||||
this.timings.query = 0;
|
|
||||||
this.timings.render = 0;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('panel count: ' + this.panelsInitCount);
|
|
||||||
console.log('total query: ' + this.timings.query);
|
|
||||||
console.log('total render: ' + this.timings.render);
|
|
||||||
console.log('avg render: ' + this.timings.render / this.panelsInitCount);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboardFetched() {
|
|
||||||
this.timings.dashboardLoadStart = new Date().getTime();
|
|
||||||
this.panelsInitCount = 0;
|
|
||||||
this.digestCounter = 0;
|
|
||||||
this.panelsInitCount = 0;
|
|
||||||
this.panelsRendered = 0;
|
|
||||||
this.timings.query = 0;
|
|
||||||
this.timings.render = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboardInitialized() {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Dashboard::Performance Total Digests: ' + this.digestCounter);
|
|
||||||
console.log('Dashboard::Performance Total Watchers: ' + this.getTotalWatcherCount());
|
|
||||||
console.log('Dashboard::Performance Total ScopeCount: ' + this.scopeCount);
|
|
||||||
|
|
||||||
const timeTaken = this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart;
|
|
||||||
console.log('Dashboard::Performance All panels initialized in ' + timeTaken + ' ms');
|
|
||||||
|
|
||||||
// measure digest performance
|
|
||||||
const rootDigestStart = window.performance.now();
|
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
this.$rootScope.$apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Dashboard::Performance Root Digest ' + (window.performance.now() - rootDigestStart) / 30);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalWatcherCount() {
|
|
||||||
let count = 0;
|
|
||||||
let scopes = 0;
|
|
||||||
const root = $(document.getElementsByTagName('body'));
|
|
||||||
|
|
||||||
const f = element => {
|
|
||||||
if (element.data().hasOwnProperty('$scope')) {
|
|
||||||
scopes++;
|
|
||||||
angular.forEach(element.data().$scope.$$watchers, () => {
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.forEach(element.children(), childElement => {
|
|
||||||
f($(childElement));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
f(root);
|
|
||||||
this.scopeCount = scopes;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingCompleted(panelId, panelTimings) {
|
|
||||||
// add render counter to root scope
|
// add render counter to root scope
|
||||||
// used by phantomjs render.js to know when panel has rendered
|
// used by phantomjs render.js to know when panel has rendered
|
||||||
this.panelsRendered = (this.panelsRendered || 0) + 1;
|
this.panelsRendered = (this.panelsRendered || 0) + 1;
|
||||||
@ -108,21 +22,6 @@ export class Profiler {
|
|||||||
// this window variable is used by backend rendering tools to know
|
// this window variable is used by backend rendering tools to know
|
||||||
// all panels have completed rendering
|
// all panels have completed rendering
|
||||||
this.window.panelsRendered = this.panelsRendered;
|
this.window.panelsRendered = this.panelsRendered;
|
||||||
|
|
||||||
if (this.enabled) {
|
|
||||||
panelTimings.renderEnd = new Date().getTime();
|
|
||||||
this.timings.query += panelTimings.queryEnd - panelTimings.queryStart;
|
|
||||||
this.timings.render += panelTimings.renderEnd - panelTimings.renderStart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panelInitialized() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.panelsInitCount++;
|
|
||||||
this.timings.lastPanelInitializedAt = new Date().getTime();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
83
public/app/core/redux/actionCreatorFactory.test.ts
Normal file
83
public/app/core/redux/actionCreatorFactory.test.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
actionCreatorFactory,
|
||||||
|
resetAllActionCreatorTypes,
|
||||||
|
noPayloadActionCreatorFactory,
|
||||||
|
} from './actionCreatorFactory';
|
||||||
|
|
||||||
|
interface Dummy {
|
||||||
|
n: number;
|
||||||
|
s: string;
|
||||||
|
o: {
|
||||||
|
n: number;
|
||||||
|
s: string;
|
||||||
|
b: boolean;
|
||||||
|
};
|
||||||
|
b: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = (payload?: Dummy) => {
|
||||||
|
resetAllActionCreatorTypes();
|
||||||
|
const actionCreator = actionCreatorFactory<Dummy>('dummy').create();
|
||||||
|
const noPayloadactionCreator = noPayloadActionCreatorFactory('NoPayload').create();
|
||||||
|
const result = actionCreator(payload);
|
||||||
|
const noPayloadResult = noPayloadactionCreator();
|
||||||
|
|
||||||
|
return { actionCreator, noPayloadactionCreator, result, noPayloadResult };
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('actionCreatorFactory', () => {
|
||||||
|
describe('when calling create', () => {
|
||||||
|
it('then it should create correct type string', () => {
|
||||||
|
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
|
||||||
|
const { actionCreator, result } = setup(payload);
|
||||||
|
|
||||||
|
expect(actionCreator.type).toEqual('dummy');
|
||||||
|
expect(result.type).toEqual('dummy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then it should create correct payload', () => {
|
||||||
|
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
|
||||||
|
const { result } = setup(payload);
|
||||||
|
|
||||||
|
expect(result.payload).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when calling create with existing type', () => {
|
||||||
|
it('then it should throw error', () => {
|
||||||
|
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
|
||||||
|
setup(payload);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
noPayloadActionCreatorFactory('DuMmY').create();
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('noPayloadActionCreatorFactory', () => {
|
||||||
|
describe('when calling create', () => {
|
||||||
|
it('then it should create correct type string', () => {
|
||||||
|
const { noPayloadResult, noPayloadactionCreator } = setup();
|
||||||
|
|
||||||
|
expect(noPayloadactionCreator.type).toEqual('NoPayload');
|
||||||
|
expect(noPayloadResult.type).toEqual('NoPayload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then it should create correct payload', () => {
|
||||||
|
const { noPayloadResult } = setup();
|
||||||
|
|
||||||
|
expect(noPayloadResult.payload).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when calling create with existing type', () => {
|
||||||
|
it('then it should throw error', () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
actionCreatorFactory<Dummy>('nOpAyLoAd').create();
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
57
public/app/core/redux/actionCreatorFactory.ts
Normal file
57
public/app/core/redux/actionCreatorFactory.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Action } from 'redux';
|
||||||
|
|
||||||
|
const allActionCreators: string[] = [];
|
||||||
|
|
||||||
|
export interface ActionOf<Payload> extends Action {
|
||||||
|
readonly type: string;
|
||||||
|
readonly payload: Payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionCreator<Payload> {
|
||||||
|
readonly type: string;
|
||||||
|
(payload: Payload): ActionOf<Payload>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoPayloadActionCreator {
|
||||||
|
readonly type: string;
|
||||||
|
(): ActionOf<undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionCreatorFactory<Payload> {
|
||||||
|
create: () => ActionCreator<Payload>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoPayloadActionCreatorFactory {
|
||||||
|
create: () => NoPayloadActionCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionCreatorFactory = <Payload>(type: string): ActionCreatorFactory<Payload> => {
|
||||||
|
const create = (): ActionCreator<Payload> => {
|
||||||
|
return Object.assign((payload: Payload): ActionOf<Payload> => ({ type, payload }), { type });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) {
|
||||||
|
throw new Error(`There is already an actionCreator defined with the type ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
allActionCreators.push(type);
|
||||||
|
|
||||||
|
return { create };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const noPayloadActionCreatorFactory = (type: string): NoPayloadActionCreatorFactory => {
|
||||||
|
const create = (): NoPayloadActionCreator => {
|
||||||
|
return Object.assign((): ActionOf<undefined> => ({ type, payload: undefined }), { type });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) {
|
||||||
|
throw new Error(`There is already an actionCreator defined with the type ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
allActionCreators.push(type);
|
||||||
|
|
||||||
|
return { create };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should only be used by tests
|
||||||
|
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);
|
4
public/app/core/redux/index.ts
Normal file
4
public/app/core/redux/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { actionCreatorFactory } from './actionCreatorFactory';
|
||||||
|
import { reducerFactory } from './reducerFactory';
|
||||||
|
|
||||||
|
export { actionCreatorFactory, reducerFactory };
|
97
public/app/core/redux/reducerFactory.test.ts
Normal file
97
public/app/core/redux/reducerFactory.test.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { reducerFactory } from './reducerFactory';
|
||||||
|
import { actionCreatorFactory, ActionOf } from './actionCreatorFactory';
|
||||||
|
|
||||||
|
interface DummyReducerState {
|
||||||
|
n: number;
|
||||||
|
s: string;
|
||||||
|
b: boolean;
|
||||||
|
o: {
|
||||||
|
n: number;
|
||||||
|
s: string;
|
||||||
|
b: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dummyReducerIntialState: DummyReducerState = {
|
||||||
|
n: 1,
|
||||||
|
s: 'One',
|
||||||
|
b: true,
|
||||||
|
o: {
|
||||||
|
n: 2,
|
||||||
|
s: 'two',
|
||||||
|
b: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dummyActionCreator = actionCreatorFactory<DummyReducerState>('dummy').create();
|
||||||
|
|
||||||
|
const dummyReducer = reducerFactory(dummyReducerIntialState)
|
||||||
|
.addMapper({
|
||||||
|
filter: dummyActionCreator,
|
||||||
|
mapper: (state, action) => ({ ...state, ...action.payload }),
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
|
describe('reducerFactory', () => {
|
||||||
|
describe('given it is created with a defined handler', () => {
|
||||||
|
describe('when reducer is called with no state', () => {
|
||||||
|
describe('and with an action that the handler can not handle', () => {
|
||||||
|
it('then the resulting state should be intial state', () => {
|
||||||
|
const result = dummyReducer(undefined as DummyReducerState, {} as ActionOf<any>);
|
||||||
|
|
||||||
|
expect(result).toEqual(dummyReducerIntialState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and with an action that the handler can handle', () => {
|
||||||
|
it('then the resulting state should correct', () => {
|
||||||
|
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } };
|
||||||
|
const result = dummyReducer(undefined as DummyReducerState, dummyActionCreator(payload));
|
||||||
|
|
||||||
|
expect(result).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when reducer is called with a state', () => {
|
||||||
|
describe('and with an action that the handler can not handle', () => {
|
||||||
|
it('then the resulting state should be intial state', () => {
|
||||||
|
const result = dummyReducer(dummyReducerIntialState, {} as ActionOf<any>);
|
||||||
|
|
||||||
|
expect(result).toEqual(dummyReducerIntialState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and with an action that the handler can handle', () => {
|
||||||
|
it('then the resulting state should correct', () => {
|
||||||
|
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } };
|
||||||
|
const result = dummyReducer(dummyReducerIntialState, dummyActionCreator(payload));
|
||||||
|
|
||||||
|
expect(result).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('given a handler is added', () => {
|
||||||
|
describe('when a handler with the same creator is added', () => {
|
||||||
|
it('then is should throw', () => {
|
||||||
|
const faultyReducer = reducerFactory(dummyReducerIntialState).addMapper({
|
||||||
|
filter: dummyActionCreator,
|
||||||
|
mapper: (state, action) => {
|
||||||
|
return { ...state, ...action.payload };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
faultyReducer.addMapper({
|
||||||
|
filter: dummyActionCreator,
|
||||||
|
mapper: state => {
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
45
public/app/core/redux/reducerFactory.ts
Normal file
45
public/app/core/redux/reducerFactory.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { ActionOf, ActionCreator } from './actionCreatorFactory';
|
||||||
|
import { Reducer } from 'redux';
|
||||||
|
|
||||||
|
export type Mapper<State, Payload> = (state: State, action: ActionOf<Payload>) => State;
|
||||||
|
|
||||||
|
export interface MapperConfig<State, Payload> {
|
||||||
|
filter: ActionCreator<Payload>;
|
||||||
|
mapper: Mapper<State, Payload>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddMapper<State> {
|
||||||
|
addMapper: <Payload>(config: MapperConfig<State, Payload>) => CreateReducer<State>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateReducer<State> extends AddMapper<State> {
|
||||||
|
create: () => Reducer<State, ActionOf<any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducerFactory = <State>(initialState: State): AddMapper<State> => {
|
||||||
|
const allMappers: { [key: string]: Mapper<State, any> } = {};
|
||||||
|
|
||||||
|
const addMapper = <Payload>(config: MapperConfig<State, Payload>): CreateReducer<State> => {
|
||||||
|
if (allMappers[config.filter.type]) {
|
||||||
|
throw new Error(`There is already a mapper defined with the type ${config.filter.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
allMappers[config.filter.type] = config.mapper;
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = (): Reducer<State, ActionOf<any>> => (state: State = initialState, action: ActionOf<any>): State => {
|
||||||
|
const mapper = allMappers[action.type];
|
||||||
|
|
||||||
|
if (mapper) {
|
||||||
|
return mapper(state, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance: CreateReducer<State> = { addMapper, create };
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import config from 'app/core/config';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||||
|
|
||||||
export class BackendSrv {
|
export class BackendSrv {
|
||||||
@ -103,9 +104,16 @@ export class BackendSrv {
|
|||||||
err => {
|
err => {
|
||||||
// handle unauthorized
|
// handle unauthorized
|
||||||
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
|
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
|
||||||
return this.loginPing().then(() => {
|
return this.loginPing()
|
||||||
|
.then(() => {
|
||||||
options.retry = 1;
|
options.retry = 1;
|
||||||
return this.request(options);
|
return this.request(options);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.status === 401) {
|
||||||
|
window.location.href = config.appSubUrl + '/logout';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,12 +192,19 @@ export class BackendSrv {
|
|||||||
|
|
||||||
// handle unauthorized for backend requests
|
// handle unauthorized for backend requests
|
||||||
if (requestIsLocal && firstAttempt && err.status === 401) {
|
if (requestIsLocal && firstAttempt && err.status === 401) {
|
||||||
return this.loginPing().then(() => {
|
return this.loginPing()
|
||||||
|
.then(() => {
|
||||||
options.retry = 1;
|
options.retry = 1;
|
||||||
if (canceler) {
|
if (canceler) {
|
||||||
canceler.resolve();
|
canceler.resolve();
|
||||||
}
|
}
|
||||||
return this.datasourceRequest(options);
|
return this.datasourceRequest(options);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.status === 401) {
|
||||||
|
window.location.href = config.appSubUrl + '/logout';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +249,7 @@ export class KeybindingSrv {
|
|||||||
if (panelInfo.panel.legend) {
|
if (panelInfo.panel.legend) {
|
||||||
const panelRef = dashboard.getPanelById(dashboard.meta.focusPanelId);
|
const panelRef = dashboard.getPanelById(dashboard.meta.focusPanelId);
|
||||||
panelRef.legend.show = !panelRef.legend.show;
|
panelRef.legend.show = !panelRef.legend.show;
|
||||||
panelRef.refresh();
|
panelRef.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
|||||||
datasource: null,
|
datasource: null,
|
||||||
queries: [],
|
queries: [],
|
||||||
range: DEFAULT_RANGE,
|
range: DEFAULT_RANGE,
|
||||||
|
ui: {
|
||||||
|
showingGraph: true,
|
||||||
|
showingTable: true,
|
||||||
|
showingLogs: true,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('state functions', () => {
|
describe('state functions', () => {
|
||||||
@ -69,9 +74,11 @@ describe('state functions', () => {
|
|||||||
to: 'now',
|
to: 'now',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(serializeStateToUrlParam(state)).toBe(
|
expect(serializeStateToUrlParam(state)).toBe(
|
||||||
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
||||||
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
|
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' +
|
||||||
|
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -93,7 +100,7 @@ describe('state functions', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(serializeStateToUrlParam(state, true)).toBe(
|
expect(serializeStateToUrlParam(state, true)).toBe(
|
||||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -118,7 +125,28 @@ describe('state functions', () => {
|
|||||||
};
|
};
|
||||||
const serialized = serializeStateToUrlParam(state);
|
const serialized = serializeStateToUrlParam(state);
|
||||||
const parsed = parseUrlState(serialized);
|
const parsed = parseUrlState(serialized);
|
||||||
|
expect(state).toMatchObject(parsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can parse the compact serialized state into the original state', () => {
|
||||||
|
const state = {
|
||||||
|
...DEFAULT_EXPLORE_STATE,
|
||||||
|
datasource: 'foo',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
expr: 'metric{test="a/b"}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expr: 'super{foo="x/z"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
range: {
|
||||||
|
from: 'now - 5h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const serialized = serializeStateToUrlParam(state, true);
|
||||||
|
const parsed = parseUrlState(serialized);
|
||||||
expect(state).toMatchObject(parsed);
|
expect(state).toMatchObject(parsed);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,6 +27,12 @@ export const DEFAULT_RANGE = {
|
|||||||
to: 'now',
|
to: 'now',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_UI_STATE = {
|
||||||
|
showingTable: true,
|
||||||
|
showingGraph: true,
|
||||||
|
showingLogs: true,
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
|
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
|
||||||
@ -147,7 +153,12 @@ export function buildQueryTransaction(
|
|||||||
|
|
||||||
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||||
|
|
||||||
|
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
|
||||||
|
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
|
||||||
|
|
||||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||||
|
let uiState = DEFAULT_UI_STATE;
|
||||||
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(decodeURI(initial));
|
const parsed = JSON.parse(decodeURI(initial));
|
||||||
@ -160,20 +171,41 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
to: parsed[1],
|
to: parsed[1],
|
||||||
};
|
};
|
||||||
const datasource = parsed[2];
|
const datasource = parsed[2];
|
||||||
const queries = parsed.slice(3);
|
let queries = [];
|
||||||
return { datasource, queries, range };
|
|
||||||
|
parsed.slice(3).forEach(segment => {
|
||||||
|
if (isMetricSegment(segment)) {
|
||||||
|
queries = [...queries, segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUISegment(segment)) {
|
||||||
|
uiState = {
|
||||||
|
showingGraph: segment.ui[0],
|
||||||
|
showingLogs: segment.ui[1],
|
||||||
|
showingTable: segment.ui[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { datasource, queries, range, ui: uiState };
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { datasource: null, queries: [], range: DEFAULT_RANGE };
|
return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: uiState };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
return JSON.stringify([
|
||||||
|
urlState.range.from,
|
||||||
|
urlState.range.to,
|
||||||
|
urlState.datasource,
|
||||||
|
...urlState.queries,
|
||||||
|
{ ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] },
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return JSON.stringify(urlState);
|
return JSON.stringify(urlState);
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,18 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
collapsed: this.props.panel.collapsed,
|
collapsed: this.props.panel.collapsed,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.toggle = this.toggle.bind(this);
|
appEvents.on('template-variable-value-updated', this.onVariableUpdated);
|
||||||
this.openSettings = this.openSettings.bind(this);
|
|
||||||
this.delete = this.delete.bind(this);
|
|
||||||
this.update = this.update.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
componentWillUnmount() {
|
||||||
|
appEvents.off('template-variable-value-updated', this.onVariableUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
onVariableUpdated = () => {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggle = () => {
|
||||||
this.props.dashboard.toggleRow(this.props.panel);
|
this.props.dashboard.toggleRow(this.props.panel);
|
||||||
|
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
@ -32,23 +37,23 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
onUpdate = () => {
|
||||||
this.props.dashboard.processRepeats();
|
this.props.dashboard.processRepeats();
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
openSettings() {
|
onOpenSettings = () => {
|
||||||
appEvents.emit('show-modal', {
|
appEvents.emit('show-modal', {
|
||||||
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
|
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
|
||||||
modalClass: 'modal--narrow',
|
modalClass: 'modal--narrow',
|
||||||
model: {
|
model: {
|
||||||
row: this.props.panel,
|
row: this.props.panel,
|
||||||
onUpdated: this.update.bind(this),
|
onUpdated: this.onUpdate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
onDelete = () => {
|
||||||
appEvents.emit('confirm-modal', {
|
appEvents.emit('confirm-modal', {
|
||||||
title: 'Delete Row',
|
title: 'Delete Row',
|
||||||
text: 'Are you sure you want to remove this row and all its panels?',
|
text: 'Are you sure you want to remove this row and all its panels?',
|
||||||
@ -81,7 +86,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<a className="dashboard-row__title pointer" onClick={this.toggle}>
|
<a className="dashboard-row__title pointer" onClick={this.onToggle}>
|
||||||
<i className={chevronClass} />
|
<i className={chevronClass} />
|
||||||
{title}
|
{title}
|
||||||
<span className="dashboard-row__panel_count">
|
<span className="dashboard-row__panel_count">
|
||||||
@ -90,16 +95,16 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
</a>
|
</a>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="dashboard-row__actions">
|
<div className="dashboard-row__actions">
|
||||||
<a className="pointer" onClick={this.openSettings}>
|
<a className="pointer" onClick={this.onOpenSettings}>
|
||||||
<i className="fa fa-cog" />
|
<i className="fa fa-cog" />
|
||||||
</a>
|
</a>
|
||||||
<a className="pointer" onClick={this.delete}>
|
<a className="pointer" onClick={this.onDelete}>
|
||||||
<i className="fa fa-trash" />
|
<i className="fa fa-trash" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.state.collapsed === true && (
|
{this.state.collapsed === true && (
|
||||||
<div className="dashboard-row__toggle-target" onClick={this.toggle}>
|
<div className="dashboard-row__toggle-target" onClick={this.onToggle}>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
|
export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl';
|
||||||
export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
|
export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl';
|
||||||
|
export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl';
|
||||||
|
123
public/app/features/dashboard/containers/SoloPanelPage.tsx
Normal file
123
public/app/features/dashboard/containers/SoloPanelPage.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// Utils & Services
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import locationUtil from 'app/core/utils/location_util';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { DashboardPanel } from '../dashgrid/DashboardPanel';
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { PanelModel, DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
panelId: string;
|
||||||
|
urlUid?: string;
|
||||||
|
urlSlug?: string;
|
||||||
|
urlType?: string;
|
||||||
|
$scope: any;
|
||||||
|
$injector: any;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
panel: PanelModel | null;
|
||||||
|
dashboard: DashboardModel | null;
|
||||||
|
notFound: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SoloPanelPage extends Component<Props, State> {
|
||||||
|
|
||||||
|
state: State = {
|
||||||
|
panel: null,
|
||||||
|
dashboard: null,
|
||||||
|
notFound: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { $injector, $scope, urlUid, urlType, urlSlug } = this.props;
|
||||||
|
|
||||||
|
// handle old urls with no uid
|
||||||
|
if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) {
|
||||||
|
this.redirectToNewUrl();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv');
|
||||||
|
|
||||||
|
// subscribe to event to know when dashboard controller is done with inititalization
|
||||||
|
appEvents.on('dashboard-initialized', this.onDashoardInitialized);
|
||||||
|
|
||||||
|
dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => {
|
||||||
|
result.meta.soloMode = true;
|
||||||
|
$scope.initDashboard(result, $scope);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectToNewUrl() {
|
||||||
|
getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => {
|
||||||
|
if (res) {
|
||||||
|
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
||||||
|
this.props.updateLocation(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDashoardInitialized = () => {
|
||||||
|
const { $scope, panelId } = this.props;
|
||||||
|
|
||||||
|
const dashboard: DashboardModel = $scope.dashboard;
|
||||||
|
const panel = dashboard.getPanelById(parseInt(panelId, 10));
|
||||||
|
|
||||||
|
if (!panel) {
|
||||||
|
this.setState({ notFound: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ dashboard, panel });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { panelId } = this.props;
|
||||||
|
const { notFound, panel, dashboard } = this.state;
|
||||||
|
|
||||||
|
if (notFound) {
|
||||||
|
return (
|
||||||
|
<div className="alert alert-error">
|
||||||
|
Panel with id { panelId } not found
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!panel) {
|
||||||
|
return <div>Loading & initializing dashboard</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel-solo">
|
||||||
|
<DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
|
urlUid: state.location.routeParams.uid,
|
||||||
|
urlSlug: state.location.routeParams.slug,
|
||||||
|
urlType: state.location.routeParams.type,
|
||||||
|
panelId: state.location.query.panelId
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
updateLocation
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage));
|
@ -8,13 +8,21 @@ import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource
|
|||||||
// Utils
|
// Utils
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
// Types
|
// Types
|
||||||
import { DataQueryOptions, DataQueryResponse, LoadingState, TimeRange, TimeSeries } from '@grafana/ui/src/types';
|
import {
|
||||||
|
DataQueryOptions,
|
||||||
|
DataQueryResponse,
|
||||||
|
LoadingState,
|
||||||
|
PanelData,
|
||||||
|
TableData,
|
||||||
|
TimeRange,
|
||||||
|
TimeSeries,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||||
|
|
||||||
interface RenderProps {
|
interface RenderProps {
|
||||||
loading: LoadingState;
|
loading: LoadingState;
|
||||||
timeSeries: TimeSeries[];
|
panelData: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -127,9 +135,7 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
cacheTimeout: null,
|
cacheTimeout: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Issuing DataPanel query', queryOptions);
|
|
||||||
const resp = await ds.query(queryOptions);
|
const resp = await ds.query(queryOptions);
|
||||||
console.log('Issuing DataPanel query Resp', resp);
|
|
||||||
|
|
||||||
if (this.isUnmounted) {
|
if (this.isUnmounted) {
|
||||||
return;
|
return;
|
||||||
@ -160,11 +166,27 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getPanelData = () => {
|
||||||
|
const { response } = this.state;
|
||||||
|
|
||||||
|
if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
|
||||||
|
return {
|
||||||
|
tableData: response.data[0] as TableData,
|
||||||
|
timeSeries: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeSeries: response.data as TimeSeries[],
|
||||||
|
tableData: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { queries } = this.props;
|
const { queries } = this.props;
|
||||||
const { response, loading, isFirstLoad } = this.state;
|
const { loading, isFirstLoad } = this.state;
|
||||||
|
|
||||||
const timeSeries = response.data;
|
const panelData = this.getPanelData();
|
||||||
|
|
||||||
if (isFirstLoad && loading === LoadingState.Loading) {
|
if (isFirstLoad && loading === LoadingState.Loading) {
|
||||||
return this.renderLoadingStates();
|
return this.renderLoadingStates();
|
||||||
@ -190,8 +212,8 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.props.children({
|
{this.props.children({
|
||||||
timeSeries,
|
|
||||||
loading,
|
loading,
|
||||||
|
panelData,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -12,12 +12,12 @@ import { DataPanel } from './DataPanel';
|
|||||||
// Utils
|
// Utils
|
||||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||||
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
||||||
|
import { profiler } from 'app/core/profiler';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { PanelModel } from '../state/PanelModel';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { DashboardModel } from '../state/DashboardModel';
|
|
||||||
import { PanelPlugin } from 'app/types';
|
import { PanelPlugin } from 'app/types';
|
||||||
import { TimeRange } from '@grafana/ui';
|
import { TimeRange, LoadingState } from '@grafana/ui';
|
||||||
|
|
||||||
import variables from 'sass/_variables.scss';
|
import variables from 'sass/_variables.scss';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
@ -94,16 +94,22 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
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 { panel, plugin } = this.props;
|
||||||
const { timeRange, renderCounter } = this.state;
|
const { timeRange, renderCounter } = this.state;
|
||||||
const PanelComponent = plugin.exports.Panel;
|
const PanelComponent = plugin.exports.Panel;
|
||||||
|
|
||||||
|
// This is only done to increase a counter that is used by backend
|
||||||
|
// image rendering (phantomjs/headless chrome) to know when to capture image
|
||||||
|
if (loading === LoadingState.Done) {
|
||||||
|
profiler.renderingCompleted(panel.id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-content">
|
<div className="panel-content">
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
timeSeries={timeSeries}
|
panelData={panelData}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||||
width={width - 2 * variables.panelHorizontalPadding}
|
width={width - 2 * variables.panelHorizontalPadding}
|
||||||
@ -139,7 +145,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
scopedVars={panel.scopedVars}
|
scopedVars={panel.scopedVars}
|
||||||
links={panel.links}
|
links={panel.links}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{panel.snapshotData ? (
|
{panel.snapshotData ? (
|
||||||
this.renderPanel(false, panel.snapshotData, width, height)
|
this.renderPanel(false, panel.snapshotData, width, height)
|
||||||
) : (
|
) : (
|
||||||
@ -152,8 +157,8 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
refreshCounter={refreshCounter}
|
refreshCounter={refreshCounter}
|
||||||
onDataResponse={this.onDataResponse}
|
onDataResponse={this.onDataResponse}
|
||||||
>
|
>
|
||||||
{({ loading, timeSeries }) => {
|
{({ loading, panelData }) => {
|
||||||
return this.renderPanel(loading, timeSeries, width, height);
|
return this.renderPanel(loading, panelData, width, height);
|
||||||
}}
|
}}
|
||||||
</DataPanel>
|
</DataPanel>
|
||||||
)}
|
)}
|
||||||
|
@ -101,17 +101,6 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-editor-container__editor">
|
<div className="panel-editor-container__editor">
|
||||||
{
|
|
||||||
// <div className="panel-editor__close">
|
|
||||||
// <i className="fa fa-arrow-left" />
|
|
||||||
// </div>
|
|
||||||
// <div className="panel-editor-resizer">
|
|
||||||
// <div className="panel-editor-resizer__handle">
|
|
||||||
// <div className="panel-editor-resizer__handle-dots" />
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="panel-editor-tabs">
|
<div className="panel-editor-tabs">
|
||||||
{tabs.map(tab => {
|
{tabs.map(tab => {
|
||||||
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
|
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
|
||||||
|
@ -133,7 +133,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
||||||
<div className="flex-grow" />
|
<div className="flex-grow-1" />
|
||||||
{!isAddingMixed && (
|
{!isAddingMixed && (
|
||||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
||||||
Add Query
|
Add Query
|
||||||
@ -165,6 +165,11 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
this.setState({ isAddingMixed: false });
|
this.setState({ isAddingMixed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onQueryChange = (query: DataQuery, index) => {
|
||||||
|
this.props.panel.changeQuery(query, index);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
|
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
this.setState({ scrollTop: target.scrollTop });
|
this.setState({ scrollTop: target.scrollTop });
|
||||||
@ -201,6 +206,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
key={query.refId}
|
key={query.refId}
|
||||||
panel={panel}
|
panel={panel}
|
||||||
query={query}
|
query={query}
|
||||||
|
onChange={query => this.onQueryChange(query, index)}
|
||||||
onRemoveQuery={this.onRemoveQuery}
|
onRemoveQuery={this.onRemoveQuery}
|
||||||
onAddQuery={this.onAddQuery}
|
onAddQuery={this.onAddQuery}
|
||||||
onMoveQuery={this.onMoveQuery}
|
onMoveQuery={this.onMoveQuery}
|
||||||
|
@ -18,6 +18,7 @@ interface Props {
|
|||||||
onAddQuery: (query?: DataQuery) => void;
|
onAddQuery: (query?: DataQuery) => void;
|
||||||
onRemoveQuery: (query: DataQuery) => void;
|
onRemoveQuery: (query: DataQuery) => void;
|
||||||
onMoveQuery: (query: DataQuery, direction: number) => void;
|
onMoveQuery: (query: DataQuery, direction: number) => void;
|
||||||
|
onChange: (query: DataQuery) => void;
|
||||||
dataSourceValue: string | null;
|
dataSourceValue: string | null;
|
||||||
inMixedMode: boolean;
|
inMixedMode: boolean;
|
||||||
}
|
}
|
||||||
@ -105,17 +106,12 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
this.setState({ isCollapsed: !this.state.isCollapsed });
|
this.setState({ isCollapsed: !this.state.isCollapsed });
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueryChange = (query: DataQuery) => {
|
onRunQuery = () => {
|
||||||
Object.assign(this.props.query, query);
|
|
||||||
this.onExecuteQuery();
|
|
||||||
};
|
|
||||||
|
|
||||||
onExecuteQuery = () => {
|
|
||||||
this.props.panel.refresh();
|
this.props.panel.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
renderPluginEditor() {
|
renderPluginEditor() {
|
||||||
const { query } = this.props;
|
const { query, onChange } = this.props;
|
||||||
const { datasource } = this.state;
|
const { datasource } = this.state;
|
||||||
|
|
||||||
if (datasource.pluginExports.QueryCtrl) {
|
if (datasource.pluginExports.QueryCtrl) {
|
||||||
@ -128,8 +124,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
<QueryEditor
|
<QueryEditor
|
||||||
query={query}
|
query={query}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
onQueryChange={this.onQueryChange}
|
onChange={onChange}
|
||||||
onExecuteQuery={this.onExecuteQuery}
|
onRunQuery={this.onRunQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -166,7 +162,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
onDisableQuery = () => {
|
onDisableQuery = () => {
|
||||||
this.props.query.hide = !this.props.query.hide;
|
this.props.query.hide = !this.props.query.hide;
|
||||||
this.onExecuteQuery();
|
this.onRunQuery();
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelById(id) {
|
getPanelById(id): PanelModel {
|
||||||
for (const panel of this.panels) {
|
for (const panel of this.panels) {
|
||||||
if (panel.id === id) {
|
if (panel.id === id) {
|
||||||
return panel;
|
return panel;
|
||||||
|
@ -5,6 +5,7 @@ import _ from 'lodash';
|
|||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
||||||
import { DataQuery, TimeSeries } from '@grafana/ui';
|
import { DataQuery, TimeSeries } from '@grafana/ui';
|
||||||
|
import { TableData } from '@grafana/ui/src';
|
||||||
|
|
||||||
export interface GridPos {
|
export interface GridPos {
|
||||||
x: number;
|
x: number;
|
||||||
@ -87,7 +88,7 @@ export class PanelModel {
|
|||||||
datasource: string;
|
datasource: string;
|
||||||
thresholds?: any;
|
thresholds?: any;
|
||||||
|
|
||||||
snapshotData?: TimeSeries[];
|
snapshotData?: TimeSeries[] | [TableData];
|
||||||
timeFrom?: any;
|
timeFrom?: any;
|
||||||
timeShift?: any;
|
timeShift?: any;
|
||||||
hideTimeOverride?: any;
|
hideTimeOverride?: any;
|
||||||
@ -268,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() {
|
destroy() {
|
||||||
this.events.emit('panel-teardown');
|
this.events.emit('panel-teardown');
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
|
@ -16,12 +16,12 @@ export class DataSourcesListItem extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card-item-body">
|
<div className="card-item-body">
|
||||||
<figure className="card-item-figure">
|
<figure className="card-item-figure">
|
||||||
<img src={dataSource.typeLogoUrl} />
|
<img src={dataSource.typeLogoUrl} alt={dataSource.name} />
|
||||||
</figure>
|
</figure>
|
||||||
<div className="card-item-details">
|
<div className="card-item-details">
|
||||||
<div className="card-item-name">
|
<div className="card-item-name">
|
||||||
{dataSource.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>
|
||||||
<div className="card-item-sub-name">{dataSource.url}</div>
|
<div className="card-item-sub-name">{dataSource.url}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import { NavModel } from 'app/types';
|
|||||||
import { DataSourceSettings } from '@grafana/ui/src/types';
|
import { DataSourceSettings } from '@grafana/ui/src/types';
|
||||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
|
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
|
||||||
|
import { setDataSourcesSearchQuery, setDataSourcesLayoutMode } from './state/actions';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
@ -13,16 +14,16 @@ const setup = (propOverrides?: object) => {
|
|||||||
loadDataSources: jest.fn(),
|
loadDataSources: jest.fn(),
|
||||||
navModel: {
|
navModel: {
|
||||||
main: {
|
main: {
|
||||||
text: 'Configuration'
|
text: 'Configuration',
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
text: 'Data Sources'
|
text: 'Data Sources',
|
||||||
}
|
},
|
||||||
} as NavModel,
|
} as NavModel,
|
||||||
dataSourcesCount: 0,
|
dataSourcesCount: 0,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
setDataSourcesSearchQuery: jest.fn(),
|
setDataSourcesSearchQuery,
|
||||||
setDataSourcesLayoutMode: jest.fn(),
|
setDataSourcesLayoutMode,
|
||||||
hasFetched: false,
|
hasFetched: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ exports[`Render should render component 1`] = `
|
|||||||
className="card-item-figure"
|
className="card-item-figure"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
alt="gdev-cloudwatch"
|
||||||
src="public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"
|
src="public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
|
@ -5,6 +5,7 @@ import { NavModel } from 'app/types';
|
|||||||
import { DataSourceSettings } from '@grafana/ui';
|
import { DataSourceSettings } from '@grafana/ui';
|
||||||
import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
|
import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
|
||||||
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
|
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
|
||||||
|
import { setDataSourceName, setIsDefault } from '../state/actions';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
@ -14,9 +15,9 @@ const setup = (propOverrides?: object) => {
|
|||||||
pageId: 1,
|
pageId: 1,
|
||||||
deleteDataSource: jest.fn(),
|
deleteDataSource: jest.fn(),
|
||||||
loadDataSource: jest.fn(),
|
loadDataSource: jest.fn(),
|
||||||
setDataSourceName: jest.fn(),
|
setDataSourceName,
|
||||||
updateDataSource: jest.fn(),
|
updateDataSource: jest.fn(),
|
||||||
setIsDefault: jest.fn(),
|
setIsDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
@ -8,131 +8,36 @@ import { UpdateLocationAction } from 'app/core/actions/location';
|
|||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import { DataSourceSettings } from '@grafana/ui/src/types';
|
import { DataSourceSettings } from '@grafana/ui/src/types';
|
||||||
import { Plugin, StoreState } from 'app/types';
|
import { Plugin, StoreState } from 'app/types';
|
||||||
|
import { actionCreatorFactory } from 'app/core/redux';
|
||||||
|
import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create();
|
||||||
LoadDataSources = 'LOAD_DATA_SOURCES',
|
|
||||||
LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
|
|
||||||
LoadedDataSourceTypes = 'LOADED_DATA_SOURCE_TYPES',
|
|
||||||
LoadDataSource = 'LOAD_DATA_SOURCE',
|
|
||||||
LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META',
|
|
||||||
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
|
|
||||||
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
|
|
||||||
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
|
|
||||||
SetDataSourceName = 'SET_DATA_SOURCE_NAME',
|
|
||||||
SetIsDefault = 'SET_IS_DEFAULT',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadDataSourcesAction {
|
export const dataSourcesLoaded = actionCreatorFactory<DataSourceSettings[]>('LOAD_DATA_SOURCES').create();
|
||||||
type: ActionTypes.LoadDataSources;
|
|
||||||
payload: DataSourceSettings[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetDataSourcesSearchQueryAction {
|
export const dataSourceMetaLoaded = actionCreatorFactory<Plugin>('LOAD_DATA_SOURCE_META').create();
|
||||||
type: ActionTypes.SetDataSourcesSearchQuery;
|
|
||||||
payload: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetDataSourcesLayoutModeAction {
|
export const dataSourceTypesLoad = noPayloadActionCreatorFactory('LOAD_DATA_SOURCE_TYPES').create();
|
||||||
type: ActionTypes.SetDataSourcesLayoutMode;
|
|
||||||
payload: LayoutMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadDataSourceTypesAction {
|
export const dataSourceTypesLoaded = actionCreatorFactory<Plugin[]>('LOADED_DATA_SOURCE_TYPES').create();
|
||||||
type: ActionTypes.LoadDataSourceTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadedDataSourceTypesAction {
|
export const setDataSourcesSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCES_SEARCH_QUERY').create();
|
||||||
type: ActionTypes.LoadedDataSourceTypes;
|
|
||||||
payload: Plugin[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetDataSourceTypeSearchQueryAction {
|
export const setDataSourcesLayoutMode = actionCreatorFactory<LayoutMode>('SET_DATA_SOURCES_LAYOUT_MODE').create();
|
||||||
type: ActionTypes.SetDataSourceTypeSearchQuery;
|
|
||||||
payload: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadDataSourceAction {
|
export const setDataSourceTypeSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCE_TYPE_SEARCH_QUERY').create();
|
||||||
type: ActionTypes.LoadDataSource;
|
|
||||||
payload: DataSourceSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadDataSourceMetaAction {
|
export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_NAME').create();
|
||||||
type: ActionTypes.LoadDataSourceMeta;
|
|
||||||
payload: Plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetDataSourceNameAction {
|
export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
|
||||||
type: ActionTypes.SetDataSourceName;
|
|
||||||
payload: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetIsDefaultAction {
|
|
||||||
type: ActionTypes.SetIsDefault;
|
|
||||||
payload: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataSourcesLoaded = (dataSources: DataSourceSettings[]): LoadDataSourcesAction => ({
|
|
||||||
type: ActionTypes.LoadDataSources,
|
|
||||||
payload: dataSources,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataSourceLoaded = (dataSource: DataSourceSettings): LoadDataSourceAction => ({
|
|
||||||
type: ActionTypes.LoadDataSource,
|
|
||||||
payload: dataSource,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction => ({
|
|
||||||
type: ActionTypes.LoadDataSourceMeta,
|
|
||||||
payload: dataSourceMeta,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataSourceTypesLoad = (): LoadDataSourceTypesAction => ({
|
|
||||||
type: ActionTypes.LoadDataSourceTypes,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadedDataSourceTypesAction => ({
|
|
||||||
type: ActionTypes.LoadedDataSourceTypes,
|
|
||||||
payload: dataSourceTypes,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({
|
|
||||||
type: ActionTypes.SetDataSourcesSearchQuery,
|
|
||||||
payload: searchQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setDataSourcesLayoutMode = (layoutMode: LayoutMode): SetDataSourcesLayoutModeAction => ({
|
|
||||||
type: ActionTypes.SetDataSourcesLayoutMode,
|
|
||||||
payload: layoutMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSearchQueryAction => ({
|
|
||||||
type: ActionTypes.SetDataSourceTypeSearchQuery,
|
|
||||||
payload: query,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setDataSourceName = (name: string) => ({
|
|
||||||
type: ActionTypes.SetDataSourceName,
|
|
||||||
payload: name,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setIsDefault = (state: boolean) => ({
|
|
||||||
type: ActionTypes.SetIsDefault,
|
|
||||||
payload: state,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| LoadDataSourcesAction
|
|
||||||
| SetDataSourcesSearchQueryAction
|
|
||||||
| SetDataSourcesLayoutModeAction
|
|
||||||
| UpdateLocationAction
|
| UpdateLocationAction
|
||||||
| LoadDataSourceTypesAction
|
|
||||||
| LoadedDataSourceTypesAction
|
|
||||||
| SetDataSourceTypeSearchQueryAction
|
|
||||||
| LoadDataSourceAction
|
|
||||||
| UpdateNavIndexAction
|
| UpdateNavIndexAction
|
||||||
| LoadDataSourceMetaAction
|
| ActionOf<DataSourceSettings>
|
||||||
| SetDataSourceNameAction
|
| ActionOf<DataSourceSettings[]>
|
||||||
| SetIsDefaultAction;
|
| ActionOf<Plugin>
|
||||||
|
| ActionOf<Plugin[]>;
|
||||||
|
|
||||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||||
|
|
||||||
|
137
public/app/features/datasources/state/reducers.test.ts
Normal file
137
public/app/features/datasources/state/reducers.test.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
|
import { dataSourcesReducer, initialState } from './reducers';
|
||||||
|
import {
|
||||||
|
dataSourcesLoaded,
|
||||||
|
dataSourceLoaded,
|
||||||
|
setDataSourcesSearchQuery,
|
||||||
|
setDataSourcesLayoutMode,
|
||||||
|
dataSourceTypesLoad,
|
||||||
|
dataSourceTypesLoaded,
|
||||||
|
setDataSourceTypeSearchQuery,
|
||||||
|
dataSourceMetaLoaded,
|
||||||
|
setDataSourceName,
|
||||||
|
setIsDefault,
|
||||||
|
} from './actions';
|
||||||
|
import { getMockDataSources, getMockDataSource } from '../__mocks__/dataSourcesMocks';
|
||||||
|
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
|
import { DataSourcesState } from 'app/types';
|
||||||
|
import { PluginMetaInfo } from '@grafana/ui';
|
||||||
|
|
||||||
|
const mockPlugin = () => ({
|
||||||
|
defaultNavUrl: 'defaultNavUrl',
|
||||||
|
enabled: true,
|
||||||
|
hasUpdate: true,
|
||||||
|
id: 'id',
|
||||||
|
info: {} as PluginMetaInfo,
|
||||||
|
latestVersion: 'latestVersion',
|
||||||
|
name: 'name',
|
||||||
|
pinned: true,
|
||||||
|
state: 'state',
|
||||||
|
type: 'type',
|
||||||
|
module: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dataSourcesReducer', () => {
|
||||||
|
describe('when dataSourcesLoaded is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const dataSources = getMockDataSources(0);
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(dataSourcesLoaded(dataSources))
|
||||||
|
.thenStateShouldEqual({ ...initialState, hasFetched: true, dataSources, dataSourcesCount: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when dataSourceLoaded is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const dataSource = getMockDataSource();
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(dataSourceLoaded(dataSource))
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSource });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when setDataSourcesSearchQuery is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(setDataSourcesSearchQuery('some query'))
|
||||||
|
.thenStateShouldEqual({ ...initialState, searchQuery: 'some query' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when setDataSourcesLayoutMode is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const layoutMode: LayoutModes = LayoutModes.Grid;
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(setDataSourcesLayoutMode(layoutMode))
|
||||||
|
.thenStateShouldEqual({ ...initialState, layoutMode: LayoutModes.Grid });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when dataSourceTypesLoad is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const state: DataSourcesState = { ...initialState, dataSourceTypes: [mockPlugin()] };
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, state)
|
||||||
|
.whenActionIsDispatched(dataSourceTypesLoad())
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSourceTypes: [], isLoadingDataSources: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when dataSourceTypesLoaded is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const dataSourceTypes = [mockPlugin()];
|
||||||
|
const state: DataSourcesState = { ...initialState, isLoadingDataSources: true };
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, state)
|
||||||
|
.whenActionIsDispatched(dataSourceTypesLoaded(dataSourceTypes))
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSourceTypes, isLoadingDataSources: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when setDataSourceTypeSearchQuery is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(setDataSourceTypeSearchQuery('type search query'))
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSourceTypeSearchQuery: 'type search query' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when dataSourceMetaLoaded is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
const dataSourceMeta = mockPlugin();
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(dataSourceMetaLoaded(dataSourceMeta))
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSourceMeta });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when setDataSourceName is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(setDataSourceName('some name'))
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSource: { name: 'some name' } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when setIsDefault is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(dataSourcesReducer, initialState)
|
||||||
|
.whenActionIsDispatched(setIsDefault(true))
|
||||||
|
.thenStateShouldEqual({ ...initialState, dataSource: { isDefault: true } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,56 +1,87 @@
|
|||||||
import { DataSourcesState, Plugin } from 'app/types';
|
import { DataSourcesState, Plugin } from 'app/types';
|
||||||
import { DataSourceSettings } from '@grafana/ui/src/types';
|
import { DataSourceSettings } from '@grafana/ui/src/types';
|
||||||
import { Action, ActionTypes } from './actions';
|
import {
|
||||||
|
dataSourceLoaded,
|
||||||
|
dataSourcesLoaded,
|
||||||
|
setDataSourcesSearchQuery,
|
||||||
|
setDataSourcesLayoutMode,
|
||||||
|
dataSourceTypesLoad,
|
||||||
|
dataSourceTypesLoaded,
|
||||||
|
setDataSourceTypeSearchQuery,
|
||||||
|
dataSourceMetaLoaded,
|
||||||
|
setDataSourceName,
|
||||||
|
setIsDefault,
|
||||||
|
} from './actions';
|
||||||
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
|
import { reducerFactory } from 'app/core/redux';
|
||||||
|
|
||||||
const initialState: DataSourcesState = {
|
export const initialState: DataSourcesState = {
|
||||||
dataSources: [] as DataSourceSettings[],
|
dataSources: [],
|
||||||
dataSource: {} as DataSourceSettings,
|
dataSource: {} as DataSourceSettings,
|
||||||
layoutMode: LayoutModes.List,
|
layoutMode: LayoutModes.List,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
dataSourcesCount: 0,
|
dataSourcesCount: 0,
|
||||||
dataSourceTypes: [] as Plugin[],
|
dataSourceTypes: [],
|
||||||
dataSourceTypeSearchQuery: '',
|
dataSourceTypeSearchQuery: '',
|
||||||
hasFetched: false,
|
hasFetched: false,
|
||||||
isLoadingDataSources: false,
|
isLoadingDataSources: false,
|
||||||
dataSourceMeta: {} as Plugin,
|
dataSourceMeta: {} as Plugin,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
|
export const dataSourcesReducer = reducerFactory(initialState)
|
||||||
switch (action.type) {
|
.addMapper({
|
||||||
case ActionTypes.LoadDataSources:
|
filter: dataSourcesLoaded,
|
||||||
return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
|
mapper: (state, action) => ({
|
||||||
|
...state,
|
||||||
case ActionTypes.LoadDataSource:
|
hasFetched: true,
|
||||||
return { ...state, dataSource: action.payload };
|
dataSources: action.payload,
|
||||||
|
dataSourcesCount: action.payload.length,
|
||||||
case ActionTypes.SetDataSourcesSearchQuery:
|
}),
|
||||||
return { ...state, searchQuery: action.payload };
|
})
|
||||||
|
.addMapper({
|
||||||
case ActionTypes.SetDataSourcesLayoutMode:
|
filter: dataSourceLoaded,
|
||||||
return { ...state, layoutMode: action.payload };
|
mapper: (state, action) => ({ ...state, dataSource: action.payload }),
|
||||||
|
})
|
||||||
case ActionTypes.LoadDataSourceTypes:
|
.addMapper({
|
||||||
return { ...state, dataSourceTypes: [], isLoadingDataSources: true };
|
filter: setDataSourcesSearchQuery,
|
||||||
|
mapper: (state, action) => ({ ...state, searchQuery: action.payload }),
|
||||||
case ActionTypes.LoadedDataSourceTypes:
|
})
|
||||||
return { ...state, dataSourceTypes: action.payload, isLoadingDataSources: false };
|
.addMapper({
|
||||||
|
filter: setDataSourcesLayoutMode,
|
||||||
case ActionTypes.SetDataSourceTypeSearchQuery:
|
mapper: (state, action) => ({ ...state, layoutMode: action.payload }),
|
||||||
return { ...state, dataSourceTypeSearchQuery: action.payload };
|
})
|
||||||
|
.addMapper({
|
||||||
case ActionTypes.LoadDataSourceMeta:
|
filter: dataSourceTypesLoad,
|
||||||
return { ...state, dataSourceMeta: action.payload };
|
mapper: state => ({ ...state, dataSourceTypes: [], isLoadingDataSources: true }),
|
||||||
|
})
|
||||||
case ActionTypes.SetDataSourceName:
|
.addMapper({
|
||||||
return { ...state, dataSource: { ...state.dataSource, name: action.payload } };
|
filter: dataSourceTypesLoaded,
|
||||||
|
mapper: (state, action) => ({
|
||||||
case ActionTypes.SetIsDefault:
|
...state,
|
||||||
return { ...state, dataSource: { ...state.dataSource, isDefault: action.payload } };
|
dataSourceTypes: action.payload,
|
||||||
}
|
isLoadingDataSources: false,
|
||||||
|
}),
|
||||||
return state;
|
})
|
||||||
};
|
.addMapper({
|
||||||
|
filter: setDataSourceTypeSearchQuery,
|
||||||
|
mapper: (state, action) => ({ ...state, dataSourceTypeSearchQuery: action.payload }),
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: dataSourceMetaLoaded,
|
||||||
|
mapper: (state, action) => ({ ...state, dataSourceMeta: action.payload }),
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: setDataSourceName,
|
||||||
|
mapper: (state, action) => ({ ...state, dataSource: { ...state.dataSource, name: action.payload } }),
|
||||||
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: setIsDefault,
|
||||||
|
mapper: (state, action) => ({
|
||||||
|
...state,
|
||||||
|
dataSource: { ...state.dataSource, isDefault: action.payload },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dataSources: dataSourcesReducer,
|
dataSources: dataSourcesReducer,
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
|
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
|
||||||
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
|
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
|
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { ExploreToolbar } from './ExploreToolbar';
|
import { ExploreToolbar } from './ExploreToolbar';
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ interface ExploreProps {
|
|||||||
supportsGraph: boolean | null;
|
supportsGraph: boolean | null;
|
||||||
supportsLogs: boolean | null;
|
supportsLogs: boolean | null;
|
||||||
supportsTable: boolean | null;
|
supportsTable: boolean | null;
|
||||||
urlState: ExploreUrlState;
|
urlState?: ExploreUrlState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,18 +107,20 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
// Don't initialize on split, but need to initialize urlparameters when present
|
// Don't initialize on split, but need to initialize urlparameters when present
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
// Load URL state and parse range
|
// Load URL state and parse range
|
||||||
const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState;
|
const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
|
||||||
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
||||||
const initialQueries: DataQuery[] = ensureQueries(queries);
|
const initialQueries: DataQuery[] = ensureQueries(queries);
|
||||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
||||||
const width = this.el ? this.el.offsetWidth : 0;
|
const width = this.el ? this.el.offsetWidth : 0;
|
||||||
|
|
||||||
this.props.initializeExplore(
|
this.props.initializeExplore(
|
||||||
exploreId,
|
exploreId,
|
||||||
initialDatasource,
|
initialDatasource,
|
||||||
initialQueries,
|
initialQueries,
|
||||||
initialRange,
|
initialRange,
|
||||||
width,
|
width,
|
||||||
this.exploreEvents
|
this.exploreEvents,
|
||||||
|
ui
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,7 +218,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
||||||
{!showingStartPage && (
|
{!showingStartPage && (
|
||||||
<>
|
<>
|
||||||
{supportsGraph && <GraphContainer exploreId={exploreId} />}
|
{supportsGraph && !supportsLogs && <GraphContainer exploreId={exploreId} />}
|
||||||
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
||||||
{supportsLogs && (
|
{supportsLogs && (
|
||||||
<LogsContainer
|
<LogsContainer
|
||||||
|
@ -3,6 +3,8 @@ import React, { PureComponent } from 'react';
|
|||||||
|
|
||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import { RawTimeRange, Switch } from '@grafana/ui';
|
import { RawTimeRange, Switch } from '@grafana/ui';
|
||||||
|
import TimeSeries from 'app/core/time_series2';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LogsDedupDescription,
|
LogsDedupDescription,
|
||||||
LogsDedupStrategy,
|
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
|
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
||||||
const getRows = () => processedRows;
|
const getRows = () => processedRows;
|
||||||
|
const timeSeries = data.series.map(series => new TimeSeries(series));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-panel">
|
<div className="logs-panel">
|
||||||
<div className="logs-panel-graph">
|
<div className="logs-panel-graph">
|
||||||
<Graph
|
<Graph
|
||||||
data={data.series}
|
data={timeSeries}
|
||||||
height="100px"
|
height="100px"
|
||||||
range={range}
|
range={range}
|
||||||
id={`explore-logs-graph-${exploreId}`}
|
id={`explore-logs-graph-${exploreId}`}
|
||||||
|
@ -104,11 +104,20 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
|
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
|
// Only update menu location when suggestion existence or text/selection changed
|
||||||
if (
|
if (value !== prevState.value || hasSuggestions(suggestions) !== hasSuggestions(prevState.suggestions)) {
|
||||||
this.state.value !== prevState.value ||
|
|
||||||
hasSuggestions(this.state.suggestions) !== hasSuggestions(prevState.suggestions)
|
|
||||||
) {
|
|
||||||
this.updateMenu();
|
this.updateMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,10 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
console.log('QueryRow will unmount');
|
||||||
|
}
|
||||||
|
|
||||||
onClickAddButton = () => {
|
onClickAddButton = () => {
|
||||||
const { exploreId, index } = this.props;
|
const { exploreId, index } = this.props;
|
||||||
this.props.addQueryRow(exploreId, index);
|
this.props.addQueryRow(exploreId, index);
|
||||||
@ -107,7 +111,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
<div className="query-row-status">
|
<div className="query-row-status">
|
||||||
<QueryTransactionStatus transactions={transactions} />
|
<QueryTransactionStatus transactions={transactions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="query-row-field">
|
<div className="query-row-field flex-shrink-1">
|
||||||
{QueryField ? (
|
{QueryField ? (
|
||||||
<QueryField
|
<QueryField
|
||||||
datasource={datasourceInstance}
|
datasource={datasourceInstance}
|
||||||
@ -131,7 +135,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline flex-shrink-0">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<button className="gf-form-label gf-form-label--btn" onClick={this.onClickClearButton}>
|
<button className="gf-form-label gf-form-label--btn" onClick={this.onClickClearButton}>
|
||||||
<i className="fa fa-times" />
|
<i className="fa fa-times" />
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
RangeScanner,
|
RangeScanner,
|
||||||
ResultType,
|
ResultType,
|
||||||
QueryTransaction,
|
QueryTransaction,
|
||||||
|
ExploreUIState,
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
@ -106,6 +107,7 @@ export interface InitializeExploreAction {
|
|||||||
exploreDatasources: DataSourceSelectItem[];
|
exploreDatasources: DataSourceSelectItem[];
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
|
ui: ExploreUIState;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
ResultType,
|
ResultType,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
QueryTransaction,
|
QueryTransaction,
|
||||||
|
ExploreUIState,
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -78,7 +79,15 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
|
|||||||
await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
|
await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
|
||||||
|
|
||||||
dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
|
dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
|
||||||
dispatch(loadDatasource(exploreId, newDataSourceInstance));
|
|
||||||
|
try {
|
||||||
|
await dispatch(loadDatasource(exploreId, newDataSourceInstance));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(runQueries(exploreId));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +163,8 @@ export function initializeExplore(
|
|||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
range: RawTimeRange,
|
range: RawTimeRange,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
eventBridge: Emitter
|
eventBridge: Emitter,
|
||||||
|
ui: ExploreUIState
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||||
@ -175,6 +185,7 @@ export function initializeExplore(
|
|||||||
exploreDatasources,
|
exploreDatasources,
|
||||||
queries,
|
queries,
|
||||||
range,
|
range,
|
||||||
|
ui,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,7 +205,14 @@ export function initializeExplore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateDatasourceInstance(exploreId, instance));
|
dispatch(updateDatasourceInstance(exploreId, instance));
|
||||||
dispatch(loadDatasource(exploreId, instance));
|
|
||||||
|
try {
|
||||||
|
await dispatch(loadDatasource(exploreId, instance));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(runQueries(exploreId, true));
|
||||||
} else {
|
} else {
|
||||||
dispatch(loadDatasourceMissing(exploreId));
|
dispatch(loadDatasourceMissing(exploreId));
|
||||||
}
|
}
|
||||||
@ -258,10 +276,7 @@ export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): Que
|
|||||||
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
||||||
* e.g., Prometheus -> Loki queries.
|
* e.g., Prometheus -> Loki queries.
|
||||||
*/
|
*/
|
||||||
export const loadDatasourceSuccess = (
|
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): LoadDatasourceSuccessAction => {
|
||||||
exploreId: ExploreId,
|
|
||||||
instance: any,
|
|
||||||
): LoadDatasourceSuccessAction => {
|
|
||||||
// Capabilities
|
// Capabilities
|
||||||
const supportsGraph = instance.meta.metrics;
|
const supportsGraph = instance.meta.metrics;
|
||||||
const supportsLogs = instance.meta.logs;
|
const supportsLogs = instance.meta.logs;
|
||||||
@ -343,8 +358,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
|||||||
|
|
||||||
// Keep ID to track selection
|
// Keep ID to track selection
|
||||||
dispatch(loadDatasourcePending(exploreId, datasourceName));
|
dispatch(loadDatasourcePending(exploreId, datasourceName));
|
||||||
|
|
||||||
let datasourceError = null;
|
let datasourceError = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const testResult = await instance.testDatasource();
|
const testResult = await instance.testDatasource();
|
||||||
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||||
@ -354,7 +369,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
|||||||
|
|
||||||
if (datasourceError) {
|
if (datasourceError) {
|
||||||
dispatch(loadDatasourceFailure(exploreId, datasourceError));
|
dispatch(loadDatasourceFailure(exploreId, datasourceError));
|
||||||
return;
|
return Promise.reject(`${datasourceName} loading failed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
|
||||||
@ -372,7 +387,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(loadDatasourceSuccess(exploreId, instance));
|
dispatch(loadDatasourceSuccess(exploreId, instance));
|
||||||
dispatch(runQueries(exploreId));
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,7 +587,7 @@ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult
|
|||||||
/**
|
/**
|
||||||
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
* Main action to run queries and dispatches sub-actions based on which result viewers are active
|
||||||
*/
|
*/
|
||||||
export function runQueries(exploreId: ExploreId) {
|
export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const {
|
const {
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
@ -596,7 +611,7 @@ export function runQueries(exploreId: ExploreId) {
|
|||||||
const interval = datasourceInstance.interval;
|
const interval = datasourceInstance.interval;
|
||||||
|
|
||||||
// Keep table queries first since they need to return quickly
|
// Keep table queries first since they need to return quickly
|
||||||
if (showingTable && supportsTable) {
|
if ((ignoreUIState || showingTable) && supportsTable) {
|
||||||
dispatch(
|
dispatch(
|
||||||
runQueriesForType(
|
runQueriesForType(
|
||||||
exploreId,
|
exploreId,
|
||||||
@ -611,7 +626,7 @@ export function runQueries(exploreId: ExploreId) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (showingGraph && supportsGraph) {
|
if ((ignoreUIState || showingGraph) && supportsGraph) {
|
||||||
dispatch(
|
dispatch(
|
||||||
runQueriesForType(
|
runQueriesForType(
|
||||||
exploreId,
|
exploreId,
|
||||||
@ -625,9 +640,10 @@ export function runQueries(exploreId: ExploreId) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (showingLogs && supportsLogs) {
|
if ((ignoreUIState || showingLogs) && supportsLogs) {
|
||||||
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
|
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(stateSave());
|
dispatch(stateSave());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -766,6 +782,11 @@ export function stateSave() {
|
|||||||
datasource: left.datasourceInstance.name,
|
datasource: left.datasourceInstance.name,
|
||||||
queries: left.modifiedQueries.map(clearQueryKeys),
|
queries: left.modifiedQueries.map(clearQueryKeys),
|
||||||
range: left.range,
|
range: left.range,
|
||||||
|
ui: {
|
||||||
|
showingGraph: left.showingGraph,
|
||||||
|
showingLogs: left.showingLogs,
|
||||||
|
showingTable: left.showingTable,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
||||||
if (split) {
|
if (split) {
|
||||||
@ -773,48 +794,64 @@ export function stateSave() {
|
|||||||
datasource: right.datasourceInstance.name,
|
datasource: right.datasourceInstance.name,
|
||||||
queries: right.modifiedQueries.map(clearQueryKeys),
|
queries: right.modifiedQueries.map(clearQueryKeys),
|
||||||
range: right.range,
|
range: right.range,
|
||||||
|
ui: {
|
||||||
|
showingGraph: right.showingGraph,
|
||||||
|
showingLogs: right.showingLogs,
|
||||||
|
showingTable: right.showingTable,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateLocation({ query: urlStates }));
|
dispatch(updateLocation({ query: urlStates }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
* Creates action to collapse graph/logs/table panel. When panel is collapsed,
|
||||||
|
* queries won't be run
|
||||||
*/
|
*/
|
||||||
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
|
const togglePanelActionCreator = (type: ActionTypes.ToggleGraph | ActionTypes.ToggleTable | ActionTypes.ToggleLogs) => (
|
||||||
|
exploreId: ExploreId
|
||||||
|
) => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
|
let shouldRunQueries;
|
||||||
if (getState().explore[exploreId].showingGraph) {
|
dispatch({ type, payload: { exploreId } });
|
||||||
|
dispatch(stateSave());
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ActionTypes.ToggleGraph:
|
||||||
|
shouldRunQueries = getState().explore[exploreId].showingGraph;
|
||||||
|
break;
|
||||||
|
case ActionTypes.ToggleLogs:
|
||||||
|
shouldRunQueries = getState().explore[exploreId].showingLogs;
|
||||||
|
break;
|
||||||
|
case ActionTypes.ToggleTable:
|
||||||
|
shouldRunQueries = getState().explore[exploreId].showingTable;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRunQueries) {
|
||||||
dispatch(runQueries(exploreId));
|
dispatch(runQueries(exploreId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
||||||
|
*/
|
||||||
|
export const toggleGraph = togglePanelActionCreator(ActionTypes.ToggleGraph);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
|
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
|
||||||
*/
|
*/
|
||||||
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
|
export const toggleLogs = togglePanelActionCreator(ActionTypes.ToggleLogs);
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
|
|
||||||
if (getState().explore[exploreId].showingLogs) {
|
|
||||||
dispatch(runQueries(exploreId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
||||||
*/
|
*/
|
||||||
export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
|
export const toggleTable = togglePanelActionCreator(ActionTypes.ToggleTable);
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
|
|
||||||
if (getState().explore[exploreId].showingTable) {
|
|
||||||
dispatch(runQueries(exploreId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets state for explore.
|
* Resets state for explore.
|
||||||
|
@ -163,7 +163,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ActionTypes.InitializeExplore: {
|
case ActionTypes.InitializeExplore: {
|
||||||
const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
|
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
@ -173,6 +173,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
|||||||
initialQueries: queries,
|
initialQueries: queries,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
modifiedQueries: queries.slice(),
|
modifiedQueries: queries.slice(),
|
||||||
|
...ui,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import './panel_header';
|
import './panel_header';
|
||||||
import './panel_directive';
|
import './panel_directive';
|
||||||
import './solo_panel_ctrl';
|
|
||||||
import './query_ctrl';
|
import './query_ctrl';
|
||||||
import './panel_editor_tab';
|
import './panel_editor_tab';
|
||||||
import './query_editor_row';
|
import './query_editor_row';
|
||||||
|
@ -16,7 +16,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
datasourceSrv: any;
|
datasourceSrv: any;
|
||||||
timeSrv: any;
|
timeSrv: any;
|
||||||
templateSrv: any;
|
templateSrv: any;
|
||||||
timing: any;
|
|
||||||
range: any;
|
range: any;
|
||||||
interval: any;
|
interval: any;
|
||||||
intervalMs: any;
|
intervalMs: any;
|
||||||
@ -81,7 +80,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
// load datasource service
|
// load datasource service
|
||||||
this.setTimeQueryStart();
|
|
||||||
this.datasourceSrv
|
this.datasourceSrv
|
||||||
.get(this.panel.datasource)
|
.get(this.panel.datasource)
|
||||||
.then(this.updateTimeRange.bind(this))
|
.then(this.updateTimeRange.bind(this))
|
||||||
@ -112,14 +110,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeQueryStart() {
|
|
||||||
this.timing.queryStart = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeQueryEnd() {
|
|
||||||
this.timing.queryEnd = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimeRange(datasource?) {
|
updateTimeRange(datasource?) {
|
||||||
this.datasource = datasource || this.datasource;
|
this.datasource = datasource || this.datasource;
|
||||||
this.range = this.timeSrv.timeRange();
|
this.range = this.timeSrv.timeRange();
|
||||||
@ -181,7 +171,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleQueryResult(result) {
|
handleQueryResult(result) {
|
||||||
this.setTimeQueryEnd();
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
// check for if data source returns subject
|
// check for if data source returns subject
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
|
||||||
import Remarkable from 'remarkable';
|
import Remarkable from 'remarkable';
|
||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
@ -13,7 +12,7 @@ import {
|
|||||||
sharePanel as sharePanelUtil,
|
sharePanel as sharePanelUtil,
|
||||||
} from 'app/features/dashboard/utils/panel';
|
} from 'app/features/dashboard/utils/panel';
|
||||||
|
|
||||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
|
import { GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
|
||||||
|
|
||||||
export class PanelCtrl {
|
export class PanelCtrl {
|
||||||
panel: any;
|
panel: any;
|
||||||
@ -31,8 +30,8 @@ export class PanelCtrl {
|
|||||||
height: any;
|
height: any;
|
||||||
containerHeight: any;
|
containerHeight: any;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
timing: any;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
timing: any;
|
||||||
maxPanelsPerRowOptions: number[];
|
maxPanelsPerRowOptions: number[];
|
||||||
|
|
||||||
constructor($scope, $injector) {
|
constructor($scope, $injector) {
|
||||||
@ -42,7 +41,7 @@ export class PanelCtrl {
|
|||||||
this.$timeout = $injector.get('$timeout');
|
this.$timeout = $injector.get('$timeout');
|
||||||
this.editorTabs = [];
|
this.editorTabs = [];
|
||||||
this.events = this.panel.events;
|
this.events = this.panel.events;
|
||||||
this.timing = {};
|
this.timing = {}; // not used but here to not break plugins
|
||||||
|
|
||||||
const plugin = config.panels[this.panel.type];
|
const plugin = config.panels[this.panel.type];
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
@ -59,7 +58,7 @@ export class PanelCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderingCompleted() {
|
renderingCompleted() {
|
||||||
profiler.renderingCompleted(this.panel.id, this.timing);
|
profiler.renderingCompleted(this.panel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
@ -200,24 +199,12 @@ export class PanelCtrl {
|
|||||||
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
|
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatePanelHeight() {
|
calculatePanelHeight(containerHeight) {
|
||||||
if (this.panel.isEditing) {
|
this.containerHeight = containerHeight;
|
||||||
this.containerHeight = $('.panel-wrapper--edit').height();
|
|
||||||
} else if (this.panel.fullscreen) {
|
|
||||||
this.containerHeight = $('.panel-wrapper--view').height();
|
|
||||||
} else {
|
|
||||||
this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.panel.soloMode) {
|
|
||||||
this.containerHeight = $(window).height();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT);
|
this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(payload?) {
|
render(payload?) {
|
||||||
this.timing.renderStart = new Date().getTime();
|
|
||||||
this.events.emit('render', payload);
|
this.events.emit('render', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctrl.events.on('panel-size-changed', () => {
|
ctrl.events.on('panel-size-changed', () => {
|
||||||
ctrl.calculatePanelHeight();
|
ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
resizeScrollableContent();
|
resizeScrollableContent();
|
||||||
ctrl.render();
|
ctrl.render();
|
||||||
@ -112,19 +112,21 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
|
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// then recalc style
|
// then recalc style
|
||||||
ctrl.calculatePanelHeight();
|
ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
|
||||||
// then wait another cycle (this might not be needed)
|
// then wait another cycle (this might not be needed)
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
ctrl.render();
|
ctrl.render();
|
||||||
resizeScrollableContent();
|
resizeScrollableContent();
|
||||||
});
|
});
|
||||||
|
}, 10);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// set initial height
|
|
||||||
ctrl.calculatePanelHeight();
|
|
||||||
|
|
||||||
ctrl.events.on('render', () => {
|
ctrl.events.on('render', () => {
|
||||||
|
// set initial height
|
||||||
|
if (!ctrl.height) {
|
||||||
|
ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
if (transparentLastState !== ctrl.panel.transparent) {
|
if (transparentLastState !== ctrl.panel.transparent) {
|
||||||
panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
|
panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
|
||||||
transparentLastState = ctrl.panel.transparent;
|
transparentLastState = ctrl.panel.transparent;
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
<div class="panel-solo" ng-if="panel">
|
|
||||||
<plugin-component type="panel">
|
|
||||||
</plugin-component>
|
|
||||||
</div>
|
|
@ -1,58 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import locationUtil from 'app/core/utils/location_util';
|
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
|
|
||||||
export class SoloPanelCtrl {
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
|
|
||||||
let panelId;
|
|
||||||
|
|
||||||
$scope.init = () => {
|
|
||||||
contextSrv.sidemenu = false;
|
|
||||||
appEvents.emit('toggle-sidemenu-hidden');
|
|
||||||
|
|
||||||
const params = $location.search();
|
|
||||||
panelId = parseInt(params.panelId, 10);
|
|
||||||
|
|
||||||
appEvents.on('dashboard-initialized', $scope.initPanelScope);
|
|
||||||
|
|
||||||
// if no uid, redirect to new route based on slug
|
|
||||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
|
||||||
backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
|
|
||||||
if (res) {
|
|
||||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
|
||||||
$location.path(url).replace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(result => {
|
|
||||||
result.meta.soloMode = true;
|
|
||||||
$scope.initDashboard(result, $scope);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.initPanelScope = () => {
|
|
||||||
const panelInfo = $scope.dashboard.getPanelInfoById(panelId);
|
|
||||||
|
|
||||||
// fake row ctrl scope
|
|
||||||
$scope.ctrl = {
|
|
||||||
dashboard: $scope.dashboard,
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.panel = panelInfo.panel;
|
|
||||||
$scope.panel.soloMode = true;
|
|
||||||
$scope.$index = 0;
|
|
||||||
|
|
||||||
if (!$scope.panel) {
|
|
||||||
$scope.appEvent('alert-error', ['Panel not found', '']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('grafana.routes').controller('SoloPanelCtrl', SoloPanelCtrl);
|
|
@ -22,6 +22,7 @@ const newVariable = index => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ElasticPipelineVariablesCtrl {
|
export class ElasticPipelineVariablesCtrl {
|
||||||
|
/** @ngInject */
|
||||||
constructor($scope) {
|
constructor($scope) {
|
||||||
$scope.variables = $scope.variables || [newVariable(1)];
|
$scope.variables = $scope.variables || [newVariable(1)];
|
||||||
|
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
// 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}
|
||||||
|
initialQuery={query}
|
||||||
|
onQueryChange={this.onFieldChange}
|
||||||
|
onPressEnter={this.onRunQuery}
|
||||||
|
/>
|
||||||
|
<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;
|
@ -12,6 +12,7 @@ import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explor
|
|||||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||||
|
import LokiDatasource from '../datasource';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
@ -65,7 +66,7 @@ interface CascaderOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LokiQueryFieldProps {
|
interface LokiQueryFieldProps {
|
||||||
datasource: any;
|
datasource: LokiDatasource;
|
||||||
error?: string | JSX.Element;
|
error?: string | JSX.Element;
|
||||||
hint?: any;
|
hint?: any;
|
||||||
history?: any[];
|
history?: any[];
|
||||||
@ -80,7 +81,7 @@ interface LokiQueryFieldState {
|
|||||||
syntaxLoaded: boolean;
|
syntaxLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
|
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
|
||||||
plugins: any[];
|
plugins: any[];
|
||||||
pluginsSearch: any[];
|
pluginsSearch: any[];
|
||||||
languageProvider: any;
|
languageProvider: any;
|
||||||
|
@ -7,6 +7,17 @@ describe('LokiDatasource', () => {
|
|||||||
url: 'myloggingurl',
|
url: 'myloggingurl',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testResp = {
|
||||||
|
data: {
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
|
||||||
|
labels: '{}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('when querying', () => {
|
describe('when querying', () => {
|
||||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||||
|
|
||||||
@ -17,7 +28,7 @@ describe('LokiDatasource', () => {
|
|||||||
|
|
||||||
test('should use default max lines when no limit given', () => {
|
test('should use default max lines when no limit given', () => {
|
||||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
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' }] });
|
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
|
||||||
|
|
||||||
ds.query(options);
|
ds.query(options);
|
||||||
@ -30,7 +41,7 @@ describe('LokiDatasource', () => {
|
|||||||
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||||
const customSettings = { ...instanceSettings, jsonData: customData };
|
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||||
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
|
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' }] });
|
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
|
||||||
ds.query(options);
|
ds.query(options);
|
||||||
@ -38,6 +49,34 @@ describe('LokiDatasource', () => {
|
|||||||
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
||||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
|
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', () => {
|
describe('when performing testDataSource', () => {
|
||||||
|
@ -32,7 +32,7 @@ function serializeParams(data: any) {
|
|||||||
.join('&');
|
.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LokiDatasource {
|
export class LokiDatasource {
|
||||||
languageProvider: LanguageProvider;
|
languageProvider: LanguageProvider;
|
||||||
maxLines: number;
|
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
|
const queryTargets = options.targets
|
||||||
.filter(target => target.expr)
|
.filter(target => target.expr && !target.hide)
|
||||||
.map(target => this.prepareQueryTarget(target, options));
|
.map(target => this.prepareQueryTarget(target, options));
|
||||||
|
|
||||||
if (queryTargets.length === 0) {
|
if (queryTargets.length === 0) {
|
||||||
return Promise.resolve({ data: [] });
|
return Promise.resolve({ data: [] });
|
||||||
}
|
}
|
||||||
@ -84,20 +85,29 @@ export default class LokiDatasource {
|
|||||||
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
|
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
|
||||||
|
|
||||||
return Promise.all(queries).then((results: any[]) => {
|
return Promise.all(queries).then((results: any[]) => {
|
||||||
// Flatten streams from multiple queries
|
const allStreams: LogsStream[] = [];
|
||||||
const allStreams: LogsStream[] = results.reduce((acc, response, i) => {
|
|
||||||
if (!response) {
|
for (let i = 0; i < results.length; i++) {
|
||||||
return acc;
|
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 => {
|
// check resultType
|
||||||
s.search = search;
|
if (options.targets[0].resultFormat === 'time_series') {
|
||||||
});
|
const logs = mergeStreamsToLogs(allStreams, this.maxLines);
|
||||||
return [...acc, ...streams];
|
logs.series = makeSeriesForLogs(logs.rows, options.intervalMs);
|
||||||
}, []);
|
return { data: logs.series };
|
||||||
|
} else {
|
||||||
return { data: allStreams };
|
return { data: allStreams };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,3 +183,5 @@ export default class LokiDatasource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default LokiDatasource;
|
||||||
|
@ -2,6 +2,7 @@ import Datasource from './datasource';
|
|||||||
|
|
||||||
import LokiStartPage from './components/LokiStartPage';
|
import LokiStartPage from './components/LokiStartPage';
|
||||||
import LokiQueryField from './components/LokiQueryField';
|
import LokiQueryField from './components/LokiQueryField';
|
||||||
|
import LokiQueryEditor from './components/LokiQueryEditor';
|
||||||
|
|
||||||
export class LokiConfigCtrl {
|
export class LokiConfigCtrl {
|
||||||
static templateUrl = 'partials/config.html';
|
static templateUrl = 'partials/config.html';
|
||||||
@ -9,6 +10,7 @@ export class LokiConfigCtrl {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Datasource,
|
Datasource,
|
||||||
|
LokiQueryEditor as QueryEditor,
|
||||||
LokiConfigCtrl as ConfigCtrl,
|
LokiConfigCtrl as ConfigCtrl,
|
||||||
LokiQueryField as ExploreQueryField,
|
LokiQueryField as ExploreQueryField,
|
||||||
LokiStartPage as ExploreStartPage,
|
LokiStartPage as ExploreStartPage,
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
"type": "datasource",
|
"type": "datasource",
|
||||||
"name": "Loki",
|
"name": "Loki",
|
||||||
"id": "loki",
|
"id": "loki",
|
||||||
"metrics": false,
|
|
||||||
|
"metrics": true,
|
||||||
"alerting": false,
|
"alerting": false,
|
||||||
"annotations": false,
|
"annotations": false,
|
||||||
"logs": true,
|
"logs": true,
|
||||||
"explore": true,
|
"explore": true,
|
||||||
"tables": false,
|
"tables": false,
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
"description": "Loki Logging Data Source for Grafana",
|
"description": "Loki Logging Data Source for Grafana",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -2,5 +2,8 @@ import { DataQuery } from '@grafana/ui/src/types';
|
|||||||
|
|
||||||
export interface LokiQuery extends DataQuery {
|
export interface LokiQuery extends DataQuery {
|
||||||
expr: string;
|
expr: string;
|
||||||
|
resultFormat?: LokiQueryResultFormats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LokiQueryResultFormats = 'time_series' | 'logs';
|
||||||
|
|
||||||
|
@ -249,15 +249,15 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||||
<div className="gf-form">
|
<div className="gf-form flex-shrink-0">
|
||||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||||
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
|
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
|
||||||
{chooserText} <i className="fa fa-caret-down" />
|
{chooserText} <i className="fa fa-caret-down" />
|
||||||
</button>
|
</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow flex-shrink-1">
|
||||||
<QueryField
|
<QueryField
|
||||||
additionalPlugins={this.plugins}
|
additionalPlugins={this.plugins}
|
||||||
cleanText={cleanText}
|
cleanText={cleanText}
|
||||||
|
@ -41,9 +41,9 @@ export class QueryEditor extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onScenarioChange = (item: SelectOptionItem) => {
|
onScenarioChange = (item: SelectOptionItem) => {
|
||||||
this.props.onQueryChange({
|
this.props.onChange({
|
||||||
|
...this.props.query,
|
||||||
scenarioId: item.value,
|
scenarioId: item.value,
|
||||||
...this.props.query
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,28 +9,39 @@ import { Gauge } from '@grafana/ui';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { GaugeOptions } from './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';
|
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
|
||||||
|
|
||||||
interface Props extends PanelProps<GaugeOptions> {}
|
interface Props extends PanelProps<GaugeOptions> {}
|
||||||
|
|
||||||
export class GaugePanel extends PureComponent<Props> {
|
export class GaugePanel extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { timeSeries, width, height, onInterpolate, options } = this.props;
|
const { panelData, width, height, onInterpolate, options } = this.props;
|
||||||
|
|
||||||
const prefix = onInterpolate(options.prefix);
|
const prefix = onInterpolate(options.prefix);
|
||||||
const suffix = onInterpolate(options.suffix);
|
const suffix = onInterpolate(options.suffix);
|
||||||
|
let value: TimeSeriesValue;
|
||||||
|
|
||||||
|
if (panelData.timeSeries) {
|
||||||
const vmSeries = processTimeSeries({
|
const vmSeries = processTimeSeries({
|
||||||
timeSeries: timeSeries,
|
timeSeries: panelData.timeSeries,
|
||||||
nullValueMode: NullValueMode.Null,
|
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 (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{(theme) => (
|
{theme => (
|
||||||
<Gauge
|
<Gauge
|
||||||
timeSeries={vmSeries}
|
value={value}
|
||||||
{...this.props.options}
|
{...this.props.options}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
@ -281,7 +281,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
toggleLegend() {
|
toggleLegend() {
|
||||||
this.panel.legend.show = !this.panel.legend.show;
|
this.panel.legend.show = !this.panel.legend.show;
|
||||||
this.refresh();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
legendValuesOptionChanged() {
|
legendValuesOptionChanged() {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<h5 class="section-heading">Options</h5>
|
<h5 class="section-heading">Options</h5>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label="Show" label-class="width-7"
|
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>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label="As Table" label-class="width-7"
|
label="As Table" label-class="width-7"
|
||||||
|
@ -16,13 +16,16 @@ interface Props extends PanelProps<Options> {}
|
|||||||
|
|
||||||
export class GraphPanel extends PureComponent<Props> {
|
export class GraphPanel extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { timeSeries, timeRange, width, height } = this.props;
|
const { panelData, timeRange, width, height } = this.props;
|
||||||
const { showLines, showBars, showPoints } = this.props.options;
|
const { showLines, showBars, showPoints } = this.props.options;
|
||||||
|
|
||||||
const vmSeries = processTimeSeries({
|
let vmSeries;
|
||||||
timeSeries: timeSeries,
|
if (panelData.timeSeries) {
|
||||||
|
vmSeries = processTimeSeries({
|
||||||
|
timeSeries: panelData.timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Ignore,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Graph
|
<Graph
|
||||||
|
@ -80,7 +80,6 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
|||||||
this.pageIndex = 0;
|
this.pageIndex = 0;
|
||||||
|
|
||||||
if (this.panel.transform === 'annotations') {
|
if (this.panel.transform === 'annotations') {
|
||||||
this.setTimeQueryStart();
|
|
||||||
return this.annotationsSrv
|
return this.annotationsSrv
|
||||||
.getAnnotations({
|
.getAnnotations({
|
||||||
dashboard: this.dashboard,
|
dashboard: this.dashboard,
|
||||||
|
@ -18,6 +18,8 @@ function WrapInProvider(store, Component, props) {
|
|||||||
export function reactContainer(
|
export function reactContainer(
|
||||||
$route,
|
$route,
|
||||||
$location,
|
$location,
|
||||||
|
$injector,
|
||||||
|
$rootScope,
|
||||||
contextSrv: ContextSrv
|
contextSrv: ContextSrv
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@ -38,7 +40,11 @@ export function reactContainer(
|
|||||||
component = component.default;
|
component = component.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = { };
|
const props = {
|
||||||
|
$injector: $injector,
|
||||||
|
$rootScope: $rootScope,
|
||||||
|
$scope: scope,
|
||||||
|
};
|
||||||
|
|
||||||
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
|
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import UsersListPage from 'app/features/users/UsersListPage';
|
|||||||
import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
|
import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
|
||||||
import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage';
|
import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage';
|
||||||
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
import OrgDetailsPage from '../features/org/OrgDetailsPage';
|
||||||
|
import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -51,16 +52,18 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
pageClass: 'page-dashboard',
|
pageClass: 'page-dashboard',
|
||||||
})
|
})
|
||||||
.when('/d-solo/:uid/:slug', {
|
.when('/d-solo/:uid/:slug', {
|
||||||
templateUrl: 'public/app/features/panel/partials/soloPanel.html',
|
template: '<react-container />',
|
||||||
controller: 'SoloPanelCtrl',
|
pageClass: 'dashboard-solo',
|
||||||
reloadOnSearch: false,
|
resolve: {
|
||||||
pageClass: 'page-dashboard',
|
component: () => SoloPanelPage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/dashboard-solo/:type/:slug', {
|
.when('/dashboard-solo/:type/:slug', {
|
||||||
templateUrl: 'public/app/features/panel/partials/soloPanel.html',
|
template: '<react-container />',
|
||||||
controller: 'SoloPanelCtrl',
|
pageClass: 'dashboard-solo',
|
||||||
reloadOnSearch: false,
|
resolve: {
|
||||||
pageClass: 'page-dashboard',
|
component: () => SoloPanelPage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/dashboard/new', {
|
.when('/dashboard/new', {
|
||||||
templateUrl: 'public/app/partials/dashboard.html',
|
templateUrl: 'public/app/partials/dashboard.html',
|
||||||
|
@ -231,10 +231,17 @@ export interface ExploreItemState {
|
|||||||
tableResult?: TableModel;
|
tableResult?: TableModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExploreUIState {
|
||||||
|
showingTable: boolean;
|
||||||
|
showingGraph: boolean;
|
||||||
|
showingLogs: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExploreUrlState {
|
export interface ExploreUrlState {
|
||||||
datasource: string;
|
datasource: string;
|
||||||
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
|
ui: ExploreUIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||||
|
@ -135,7 +135,7 @@ $input-padding-y-sm: 4px !default;
|
|||||||
$input-padding-x-lg: 20px !default;
|
$input-padding-x-lg: 20px !default;
|
||||||
$input-padding-y-lg: 10px !default;
|
$input-padding-y-lg: 10px !default;
|
||||||
|
|
||||||
$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default;
|
$input-height: 35px !default;
|
||||||
|
|
||||||
$gf-form-margin: 0.2rem;
|
$gf-form-margin: 0.2rem;
|
||||||
$gf-form-input-height: 35px;
|
$gf-form-input-height: 35px;
|
||||||
|
@ -109,6 +109,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-item-label {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-item-sub-name {
|
.card-item-sub-name {
|
||||||
color: $text-color-weak;
|
color: $text-color-weak;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
&.ace_editor {
|
&.ace_editor {
|
||||||
@include font-family-monospace();
|
@include font-family-monospace();
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
min-height: 2.6rem;
|
min-height: 3.6rem; // Include space for horizontal scrollbar
|
||||||
|
|
||||||
@include border-radius($input-border-radius-sm);
|
@include border-radius($input-border-radius-sm);
|
||||||
border: $input-btn-border-width solid $input-border-color;
|
border: $input-btn-border-width solid $input-border-color;
|
||||||
|
@ -84,6 +84,10 @@ $input-border: 1px solid $input-border-color;
|
|||||||
.gf-form + .gf-form {
|
.gf-form + .gf-form {
|
||||||
margin-left: $gf-form-margin;
|
margin-left: $gf-form-margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--nowrap {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form-button-row {
|
.gf-form-button-row {
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
border: $panel-border;
|
border: $panel-border;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
line-height: $input-line-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slate-query-field__wrapper--disabled {
|
.slate-query-field__wrapper--disabled {
|
||||||
|
@ -40,9 +40,9 @@
|
|||||||
background-color: $input-bg;
|
background-color: $input-bg;
|
||||||
border: 1px solid $input-border-color;
|
border: 1px solid $input-border-color;
|
||||||
border-radius: $input-border-radius;
|
border-radius: $input-border-radius;
|
||||||
box-sizing: content-box;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
height: $gf-form-input-height;
|
||||||
|
|
||||||
.label-tag {
|
.label-tag {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 3px 20px 3px 20px;
|
padding: 3px 20px 3px 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
background: $toolbar-bg;
|
background: $toolbar-bg;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -17,6 +17,13 @@ div.flot-text {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-solo {
|
||||||
|
.footer,
|
||||||
|
.sidemenu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.panel-solo {
|
.panel-solo {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user