Merge remote-tracking branch 'grafana/master'
* grafana/master: (54 commits) s/initialDatasourceId/initialDatasource/ Fixed issues with panel size in edit mode, fixes #14703 Tweak datetime picker layout for mobile Explore: Remember last use datasource Update yarn.lock Logs data model: add more log levels Review feedback Explore: fix loading indicator z-index on panel container Loki: change query row to be single field again Explore: logging UI style fixes Loki: query limit configurable in datasource Removed rxjs compat ldap: adds extra debug logging adds orgId to user dto for provisioned dashboards Update rxjs closes the body properly on successful webhooks makes cache mode configurable Fix general tab typos added node-sass as dev dependency, needed after I removed grunt-sass Husky and sasslint fixes, fixes #14638 ...
This commit is contained in:
+2
-1
@@ -16,6 +16,7 @@
|
||||
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
|
||||
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
||||
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
|
||||
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
|
||||
|
||||
### Bug fixes
|
||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
* **Datasource admin**: Fix for issue creating new data source when same name exists [#14467](https://github.com/grafana/grafana/issues/14467)
|
||||
* **OAuth**: Fix for oauth auto login setting, can now be set using env variable [#14435](https://github.com/grafana/grafana/issues/14435)
|
||||
* **Dashboard search**: Fix for searching tags in tags filter dropdown.
|
||||
* **Dashboard search**: Fix for searching tags in tags filter dropdown.
|
||||
|
||||
# 5.4.1 (2018-12-10)
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ server_cert_name =
|
||||
# For "sqlite3" only, path relative to data_path setting
|
||||
path = grafana.db
|
||||
|
||||
# For "sqlite3" only. cache mode setting used for connecting to the database
|
||||
cache_mode = private
|
||||
|
||||
#################################### Session #############################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
# Set to true to log the sql calls and execution times.
|
||||
log_queries =
|
||||
|
||||
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
||||
;cache_mode = private
|
||||
|
||||
#################################### Session ####################################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
FROM golang:latest
|
||||
ADD main.go /
|
||||
WORKDIR /
|
||||
RUN go build -o main .
|
||||
EXPOSE 3010
|
||||
ENTRYPOINT ["/main"]
|
||||
@@ -0,0 +1,5 @@
|
||||
alert_webhook_listener:
|
||||
build: docker/blocks/alert_webhook_listener
|
||||
network_mode: host
|
||||
ports:
|
||||
- "3010:3010"
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func hello(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("webbhook: -> %s", string(body))
|
||||
fmt.Println(line)
|
||||
io.WriteString(w, line)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", hello)
|
||||
http.ListenAndServe(":3010", nil)
|
||||
}
|
||||
@@ -31,9 +31,10 @@ auto_sign_up = true
|
||||
ldap_sync_ttl = 60
|
||||
# Limit where auth proxy requests come from by configuring a list of IP addresses.
|
||||
# This can be used to prevent users spoofing the X-WEBAUTH-USER header.
|
||||
# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
|
||||
whitelist =
|
||||
# Optionally define more headers to sync other user attributes
|
||||
# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL``
|
||||
# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`
|
||||
headers =
|
||||
```
|
||||
|
||||
|
||||
@@ -250,6 +250,12 @@ Sets the maximum amount of time a connection may be reused. The default is 14400
|
||||
|
||||
Set to `true` to log the sql calls and execution times.
|
||||
|
||||
### cache_mode
|
||||
|
||||
For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared)
|
||||
Defaults to private.
|
||||
|
||||
|
||||
<hr />
|
||||
|
||||
## [security]
|
||||
|
||||
+3
-1
@@ -6,7 +6,9 @@ module.exports = {
|
||||
},
|
||||
"moduleDirectories": ["node_modules", "public"],
|
||||
"roots": [
|
||||
"<rootDir>/public"
|
||||
"<rootDir>/public/app",
|
||||
"<rootDir>/public/test",
|
||||
"<rootDir>/packages"
|
||||
],
|
||||
"testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": [
|
||||
|
||||
+41
-16
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Torkel Ödegaard",
|
||||
"company": "Grafana Labs"
|
||||
@@ -11,14 +12,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/d3": "^4.10.1",
|
||||
"@types/enzyme": "^3.1.13",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/jquery": "^1.10.35",
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
@@ -49,15 +52,12 @@
|
||||
"grunt-cli": "~1.2.0",
|
||||
"grunt-contrib-clean": "~1.0.0",
|
||||
"grunt-contrib-compress": "^1.3.0",
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-copy": "~1.0.0",
|
||||
"grunt-contrib-cssmin": "~1.0.2",
|
||||
"grunt-exec": "^1.0.1",
|
||||
"grunt-newer": "^1.3.0",
|
||||
"grunt-notify": "^0.4.5",
|
||||
"grunt-postcss": "^0.8.0",
|
||||
"grunt-sass": "^2.0.0",
|
||||
"grunt-sass-lint": "^0.2.2",
|
||||
"grunt-sass-lint": "^0.2.4",
|
||||
"grunt-usemin": "3.1.1",
|
||||
"grunt-webpack": "^3.0.2",
|
||||
"html-loader": "^0.5.1",
|
||||
@@ -73,6 +73,7 @@
|
||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||
"ngtemplate-loader": "^2.0.1",
|
||||
"npm": "^5.4.2",
|
||||
"node-sass": "^4.11.0",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
||||
"phantomjs-prebuilt": "^2.1.15",
|
||||
"postcss-browser-reporter": "^0.5.0",
|
||||
@@ -92,6 +93,7 @@
|
||||
"tslib": "^1.9.3",
|
||||
"tslint": "^5.8.0",
|
||||
"tslint-loader": "^3.5.3",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^3.0.3",
|
||||
"uglifyjs-webpack-plugin": "^1.2.7",
|
||||
"webpack": "4.19.1",
|
||||
@@ -108,15 +110,30 @@
|
||||
"watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js",
|
||||
"build": "grunt build",
|
||||
"test": "grunt test",
|
||||
"lint": "tslint -c tslint.json --project tsconfig.json",
|
||||
"tslint": "tslint -c tslint.json --project tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"jest": "jest --notify --watch",
|
||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||
"precommit": "lint-staged && grunt precommit"
|
||||
"precommit": "grunt precommit"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged && grunt precommit"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": ["prettier --write", "git add"],
|
||||
"*.scss": ["prettier --write", "git add"],
|
||||
"*pkg/**/*.go": ["gofmt -w -s", "git add"]
|
||||
"*.{ts,tsx}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.scss": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*pkg/**/*.go": [
|
||||
"gofmt -w -s",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@@ -126,6 +143,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"angular": "1.6.6",
|
||||
"angular-bindonce": "0.3.1",
|
||||
"angular-native-dragdrop": "1.2.2",
|
||||
@@ -133,7 +151,7 @@
|
||||
"angular-sanitize": "1.6.6",
|
||||
"baron": "^3.0.3",
|
||||
"brace": "^0.10.0",
|
||||
"classnames": "^2.2.5",
|
||||
"classnames": "^2.2.6",
|
||||
"clipboard": "^1.7.1",
|
||||
"d3": "^4.11.0",
|
||||
"d3-scale-chromatic": "^1.3.0",
|
||||
@@ -152,10 +170,9 @@
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-grid-layout": "0.16.6",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"react-sizeme": "^2.3.6",
|
||||
"react-table": "^6.8.6",
|
||||
"react-transition-group": "^2.2.1",
|
||||
@@ -165,18 +182,26 @@
|
||||
"redux-thunk": "^2.3.0",
|
||||
"remarkable": "^1.7.1",
|
||||
"rst2html": "github:thoward/rst2html#990cb89",
|
||||
"rxjs": "^5.4.3",
|
||||
"rxjs": "^6.3.3",
|
||||
"slate": "^0.33.4",
|
||||
"slate-plain-serializer": "^0.5.10",
|
||||
"slate-prism": "^0.5.0",
|
||||
"slate-react": "^0.12.4",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1",
|
||||
"tslint-react": "^3.6.0"
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"caniuse-db": "1.0.30000772",
|
||||
"**/@types/react": "16.7.6"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"**/@types/*",
|
||||
"**/@types/*/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Shared build scripts
|
||||
|
||||
Shared build scripts for plugins & internal packages.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@grafana/build",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"tslint": "echo \"Nothing to do\"",
|
||||
"typecheck": "echo \"Nothing to do\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Grafana (WIP) shared component library
|
||||
|
||||
Used by internal & external plugins.
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@grafana/ui",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"tslint": "tslint -c tslint.json --project tsconfig.json",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"classnames": "^2.2.5",
|
||||
"jquery": "^3.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-virtualized": "^9.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/jquery": "^1.10.35",
|
||||
"typescript": "^3.2.2"
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import { DeleteButton } from './DeleteButton';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
describe('DeleteButton', () => {
|
||||
let wrapper;
|
||||
let deleted;
|
||||
let wrapper: any;
|
||||
let deleted: any;
|
||||
|
||||
beforeAll(() => {
|
||||
deleted = false;
|
||||
@@ -12,7 +12,8 @@ describe('DeleteButton', () => {
|
||||
function deleteItem() {
|
||||
deleted = true;
|
||||
}
|
||||
wrapper = shallow(<DeleteButton onConfirmDelete={() => deleteItem()} />);
|
||||
|
||||
wrapper = shallow(<DeleteButton onConfirm={() => deleteItem()} />);
|
||||
});
|
||||
|
||||
it('should show confirm delete when clicked', () => {
|
||||
+10
-10
@@ -1,19 +1,19 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, SyntheticEvent } from 'react';
|
||||
|
||||
export interface DeleteButtonProps {
|
||||
onConfirmDelete();
|
||||
interface Props {
|
||||
onConfirm(): void;
|
||||
}
|
||||
|
||||
export interface DeleteButtonStates {
|
||||
interface State {
|
||||
showConfirm: boolean;
|
||||
}
|
||||
|
||||
export default class DeleteButton extends PureComponent<DeleteButtonProps, DeleteButtonStates> {
|
||||
state: DeleteButtonStates = {
|
||||
export class DeleteButton extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
showConfirm: false,
|
||||
};
|
||||
|
||||
onClickDelete = event => {
|
||||
onClickDelete = (event: SyntheticEvent) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
|
||||
});
|
||||
};
|
||||
|
||||
onClickCancel = event => {
|
||||
onClickCancel = (event: SyntheticEvent) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
|
||||
};
|
||||
|
||||
render() {
|
||||
const onClickConfirm = this.props.onConfirmDelete;
|
||||
const { onConfirm } = this.props;
|
||||
let showConfirm;
|
||||
let showDeleteButton;
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
|
||||
<a className="btn btn-small" onClick={this.onClickCancel}>
|
||||
Cancel
|
||||
</a>
|
||||
<a className="btn btn-danger btn-small" onClick={onClickConfirm}>
|
||||
<a className="btn btn-danger btn-small" onClick={onConfirm}>
|
||||
Confirm Delete
|
||||
</a>
|
||||
</span>
|
||||
@@ -0,0 +1 @@
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@@ -0,0 +1 @@
|
||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { SFC, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
htmlFor?: string;
|
||||
className?: string;
|
||||
isFocused?: boolean;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
|
||||
export const GfFormLabel: SFC<Props> = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => {
|
||||
const classes = classNames('gf-form-label', className, {
|
||||
'gf-form-label--is-focused': isFocused,
|
||||
'gf-form-label--is-invalid': isInvalid,
|
||||
});
|
||||
|
||||
return (
|
||||
<label className={classes} {...rest} htmlFor={htmlFor}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
|
||||
@@ -0,0 +1 @@
|
||||
@import 'components/index';
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './components';
|
||||
export * from './visualizations';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './forms';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './series';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
interface JQueryPlot {
|
||||
(element: HTMLElement | JQuery, data: any, options: any): void;
|
||||
plugins: any[];
|
||||
}
|
||||
|
||||
interface JQueryStatic {
|
||||
plot: JQueryPlot;
|
||||
}
|
||||
|
||||
interface JQuery {
|
||||
place_tt: any;
|
||||
modal: any;
|
||||
tagsinput: any;
|
||||
typeahead: any;
|
||||
accessKey: any;
|
||||
tooltip: any;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TimeSeries, LoadingState } from './series';
|
||||
import { TimeRange } from './time';
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
timeSeries: TimeSeries[];
|
||||
timeRange: TimeRange;
|
||||
loading: LoadingState;
|
||||
options: T;
|
||||
renderCounter: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
options: T;
|
||||
onChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export interface PanelSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface PanelMenuItem {
|
||||
type?: 'submenu' | 'divider';
|
||||
text?: string;
|
||||
iconClassName?: string;
|
||||
onClick?: () => void;
|
||||
shortcut?: string;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export enum LoadingState {
|
||||
NotStarted = 'NotStarted',
|
||||
Loading = 'Loading',
|
||||
Done = 'Done',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export type TimeSeriesValue = number | null;
|
||||
|
||||
export type TimeSeriesPoints = TimeSeriesValue[][];
|
||||
|
||||
export interface TimeSeries {
|
||||
target: string;
|
||||
datapoints: TimeSeriesPoints;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
/** View model projection of a time series */
|
||||
export interface TimeSeriesVM {
|
||||
label: string;
|
||||
color: string;
|
||||
data: TimeSeriesValue[][];
|
||||
stats: TimeSeriesStats;
|
||||
}
|
||||
|
||||
export interface TimeSeriesStats {
|
||||
total: number | null;
|
||||
max: number | null;
|
||||
min: number | null;
|
||||
logmin: number;
|
||||
avg: number | null;
|
||||
current: number | null;
|
||||
first: number | null;
|
||||
delta: number;
|
||||
diff: number | null;
|
||||
range: number | null;
|
||||
timeStep: number;
|
||||
count: number;
|
||||
allIsNull: boolean;
|
||||
allIsZero: boolean;
|
||||
}
|
||||
|
||||
export enum NullValueMode {
|
||||
Null = 'null',
|
||||
Ignore = 'connected',
|
||||
AsZero = 'null as zero',
|
||||
}
|
||||
|
||||
/** View model projection of many time series */
|
||||
export interface TimeSeriesVMs {
|
||||
[index: number]: TimeSeriesVM;
|
||||
length: number;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Moment } from 'moment';
|
||||
|
||||
export interface RawTimeRange {
|
||||
from: Moment | string;
|
||||
to: Moment | string;
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
from: Moment;
|
||||
to: Moment;
|
||||
raw: RawTimeRange;
|
||||
}
|
||||
|
||||
export interface IntervalValues {
|
||||
interval: string; // 10s,5m
|
||||
intervalMs: number;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './processTimeSeries';
|
||||
@@ -0,0 +1,174 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Types
|
||||
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
|
||||
|
||||
interface Options {
|
||||
timeSeries: TimeSeries[];
|
||||
nullValueMode: NullValueMode;
|
||||
colorPalette: string[];
|
||||
}
|
||||
|
||||
export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs {
|
||||
const vmSeries = timeSeries.map((item, index) => {
|
||||
const colorIndex = index % colorPalette.length;
|
||||
const label = item.target;
|
||||
const result = [];
|
||||
|
||||
// stat defaults
|
||||
let total = 0;
|
||||
let max: TimeSeriesValue = -Number.MAX_VALUE;
|
||||
let min: TimeSeriesValue = Number.MAX_VALUE;
|
||||
let logmin = Number.MAX_VALUE;
|
||||
let avg: TimeSeriesValue = null;
|
||||
let current: TimeSeriesValue = null;
|
||||
let first: TimeSeriesValue = null;
|
||||
let delta: TimeSeriesValue = 0;
|
||||
let diff: TimeSeriesValue = null;
|
||||
let range: TimeSeriesValue = null;
|
||||
let timeStep = Number.MAX_VALUE;
|
||||
let allIsNull = true;
|
||||
let allIsZero = true;
|
||||
|
||||
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
||||
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
||||
|
||||
let currentTime: TimeSeriesValue = null;
|
||||
let currentValue: TimeSeriesValue = null;
|
||||
let nonNulls = 0;
|
||||
let previousTime: TimeSeriesValue = null;
|
||||
let previousValue = 0;
|
||||
let previousDeltaUp = true;
|
||||
|
||||
for (let i = 0; i < item.datapoints.length; i++) {
|
||||
currentValue = item.datapoints[i][0];
|
||||
currentTime = item.datapoints[i][1];
|
||||
|
||||
if (typeof currentTime !== 'number') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof currentValue !== 'number') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Due to missing values we could have different timeStep all along the series
|
||||
// so we have to find the minimum one (could occur with aggregators such as ZimSum)
|
||||
if (previousTime !== null && currentTime !== null) {
|
||||
const currentStep = currentTime - previousTime;
|
||||
if (currentStep < timeStep) {
|
||||
timeStep = currentStep;
|
||||
}
|
||||
}
|
||||
|
||||
previousTime = currentTime;
|
||||
|
||||
if (currentValue === null) {
|
||||
if (ignoreNulls) {
|
||||
continue;
|
||||
}
|
||||
if (nullAsZero) {
|
||||
currentValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentValue !== null) {
|
||||
if (_.isNumber(currentValue)) {
|
||||
total += currentValue;
|
||||
allIsNull = false;
|
||||
nonNulls++;
|
||||
}
|
||||
|
||||
if (currentValue > max) {
|
||||
max = currentValue;
|
||||
}
|
||||
|
||||
if (currentValue < min) {
|
||||
min = currentValue;
|
||||
}
|
||||
|
||||
if (first === null) {
|
||||
first = currentValue;
|
||||
} else {
|
||||
if (previousValue > currentValue) {
|
||||
// counter reset
|
||||
previousDeltaUp = false;
|
||||
if (i === item.datapoints.length - 1) {
|
||||
// reset on last
|
||||
delta += currentValue;
|
||||
}
|
||||
} else {
|
||||
if (previousDeltaUp) {
|
||||
delta += currentValue - previousValue; // normal increment
|
||||
} else {
|
||||
delta += currentValue; // account for counter reset
|
||||
}
|
||||
previousDeltaUp = true;
|
||||
}
|
||||
}
|
||||
previousValue = currentValue;
|
||||
|
||||
if (currentValue < logmin && currentValue > 0) {
|
||||
logmin = currentValue;
|
||||
}
|
||||
|
||||
if (currentValue !== 0) {
|
||||
allIsZero = false;
|
||||
}
|
||||
}
|
||||
|
||||
result.push([currentTime, currentValue]);
|
||||
}
|
||||
|
||||
if (max === -Number.MAX_VALUE) {
|
||||
max = null;
|
||||
}
|
||||
|
||||
if (min === Number.MAX_VALUE) {
|
||||
min = null;
|
||||
}
|
||||
|
||||
if (result.length && !allIsNull) {
|
||||
avg = total / nonNulls;
|
||||
current = result[result.length - 1][1];
|
||||
if (current === null && result.length > 1) {
|
||||
current = result[result.length - 2][1];
|
||||
}
|
||||
}
|
||||
|
||||
if (max !== null && min !== null) {
|
||||
range = max - min;
|
||||
}
|
||||
|
||||
if (current !== null && first !== null) {
|
||||
diff = current - first;
|
||||
}
|
||||
|
||||
const count = result.length;
|
||||
|
||||
return {
|
||||
data: result,
|
||||
label: label,
|
||||
color: colorPalette[colorIndex],
|
||||
stats: {
|
||||
total,
|
||||
min,
|
||||
max,
|
||||
current,
|
||||
logmin,
|
||||
avg,
|
||||
diff,
|
||||
delta,
|
||||
timeStep,
|
||||
range,
|
||||
count,
|
||||
first,
|
||||
allIsZero,
|
||||
allIsNull,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return vmSeries;
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
// Libraries
|
||||
import $ from 'jquery';
|
||||
import React, { PureComponent } from 'react';
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.time';
|
||||
|
||||
// Types
|
||||
import { TimeRange, TimeSeriesVMs } from 'app/types';
|
||||
import { TimeRange, TimeSeriesVMs } from '../../types';
|
||||
|
||||
interface GraphProps {
|
||||
timeSeries: TimeSeriesVMs;
|
||||
@@ -24,7 +22,7 @@ export class Graph extends PureComponent<GraphProps> {
|
||||
showBars: false,
|
||||
};
|
||||
|
||||
element: HTMLElement;
|
||||
element: HTMLElement | null;
|
||||
|
||||
componentDidUpdate() {
|
||||
this.draw();
|
||||
@@ -35,6 +33,10 @@ export class Graph extends PureComponent<GraphProps> {
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.element === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, timeSeries, timeRange, showLines, showBars, showPoints } = this.props;
|
||||
|
||||
if (!width) {
|
||||
@@ -76,7 +78,7 @@ export class Graph extends PureComponent<GraphProps> {
|
||||
max: max,
|
||||
label: 'Datetime',
|
||||
ticks: ticks,
|
||||
timeformat: time_format(ticks, min, max),
|
||||
timeformat: timeFormat(ticks, min, max),
|
||||
},
|
||||
grid: {
|
||||
minBorderMargin: 0,
|
||||
@@ -109,7 +111,7 @@ export class Graph extends PureComponent<GraphProps> {
|
||||
}
|
||||
|
||||
// Copied from graph.ts
|
||||
function time_format(ticks, min, max) {
|
||||
function timeFormat(ticks: number, min: number, max: number): string {
|
||||
if (min && max && ticks) {
|
||||
const range = max - min;
|
||||
const secPerTick = range / ticks / 1000;
|
||||
@@ -0,0 +1 @@
|
||||
export { Graph } from './Graph/Graph';
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"module": "esnext",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tslint.json"
|
||||
}
|
||||
@@ -292,6 +292,8 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
|
||||
}
|
||||
|
||||
a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
|
||||
|
||||
searchResult, err = a.conn.Search(&searchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -175,7 +175,9 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
|
||||
dto.User = &models.SignedInUser{
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
OrgId: dto.OrgId,
|
||||
}
|
||||
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -69,11 +70,14 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode/100 == 2 {
|
||||
// flushing the body enables the transport to reuse the same connection
|
||||
io.Copy(ioutil.Discard, resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,18 +29,22 @@ import (
|
||||
|
||||
// MysqlStore represents a mysql session store implementation.
|
||||
type MysqlStore struct {
|
||||
c *sql.DB
|
||||
sid string
|
||||
lock sync.RWMutex
|
||||
data map[interface{}]interface{}
|
||||
c *sql.DB
|
||||
sid string
|
||||
lock sync.RWMutex
|
||||
data map[interface{}]interface{}
|
||||
expiry int64
|
||||
dirty bool
|
||||
}
|
||||
|
||||
// NewMysqlStore creates and returns a mysql session store.
|
||||
func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore {
|
||||
func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}, expiry int64) *MysqlStore {
|
||||
return &MysqlStore{
|
||||
c: c,
|
||||
sid: sid,
|
||||
data: kv,
|
||||
c: c,
|
||||
sid: sid,
|
||||
data: kv,
|
||||
expiry: expiry,
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +54,7 @@ func (s *MysqlStore) Set(key, val interface{}) error {
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data[key] = val
|
||||
s.dirty = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,6 +72,7 @@ func (s *MysqlStore) Delete(key interface{}) error {
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.data, key)
|
||||
s.dirty = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -77,13 +83,20 @@ func (s *MysqlStore) ID() string {
|
||||
|
||||
// Release releases resource and save data to provider.
|
||||
func (s *MysqlStore) Release() error {
|
||||
newExpiry := time.Now().Unix()
|
||||
if !s.dirty && (s.expiry+60) >= newExpiry {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := session.EncodeGob(s.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?",
|
||||
data, time.Now().Unix(), s.sid)
|
||||
data, newExpiry, s.sid)
|
||||
s.dirty = false
|
||||
s.expiry = newExpiry
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -93,6 +106,7 @@ func (s *MysqlStore) Flush() error {
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.data = make(map[interface{}]interface{})
|
||||
s.dirty = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -117,11 +131,12 @@ func (p *MysqlProvider) Init(expire int64, connStr string) (err error) {
|
||||
|
||||
// Read returns raw session store by session ID.
|
||||
func (p *MysqlProvider) Read(sid string) (session.RawStore, error) {
|
||||
expiry := time.Now().Unix()
|
||||
var data []byte
|
||||
err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
|
||||
err := p.c.QueryRow("SELECT data,expiry FROM session WHERE `key`=?", sid).Scan(&data, &expiry)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)",
|
||||
sid, "", time.Now().Unix())
|
||||
sid, "", expiry)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -137,7 +152,7 @@ func (p *MysqlProvider) Read(sid string) (session.RawStore, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return NewMysqlStore(p.c, sid, kv), nil
|
||||
return NewMysqlStore(p.c, sid, kv, expiry), nil
|
||||
}
|
||||
|
||||
// Exist returns true if session with given ID exists.
|
||||
|
||||
@@ -2,12 +2,47 @@ package migrator
|
||||
|
||||
type MigrationCondition interface {
|
||||
Sql(dialect Dialect) (string, []interface{})
|
||||
IsFulfilled(results []map[string][]byte) bool
|
||||
}
|
||||
|
||||
type IfTableExistsCondition struct {
|
||||
type ExistsMigrationCondition struct{}
|
||||
|
||||
func (c *ExistsMigrationCondition) IsFulfilled(results []map[string][]byte) bool {
|
||||
return len(results) >= 1
|
||||
}
|
||||
|
||||
type NotExistsMigrationCondition struct{}
|
||||
|
||||
func (c *NotExistsMigrationCondition) IsFulfilled(results []map[string][]byte) bool {
|
||||
return len(results) == 0
|
||||
}
|
||||
|
||||
type IfIndexExistsCondition struct {
|
||||
ExistsMigrationCondition
|
||||
TableName string
|
||||
IndexName string
|
||||
}
|
||||
|
||||
func (c *IfTableExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
|
||||
return dialect.TableCheckSql(c.TableName)
|
||||
func (c *IfIndexExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
|
||||
return dialect.IndexCheckSql(c.TableName, c.IndexName)
|
||||
}
|
||||
|
||||
type IfIndexNotExistsCondition struct {
|
||||
NotExistsMigrationCondition
|
||||
TableName string
|
||||
IndexName string
|
||||
}
|
||||
|
||||
func (c *IfIndexNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
|
||||
return dialect.IndexCheckSql(c.TableName, c.IndexName)
|
||||
}
|
||||
|
||||
type IfColumnNotExistsCondition struct {
|
||||
NotExistsMigrationCondition
|
||||
TableName string
|
||||
ColumnName string
|
||||
}
|
||||
|
||||
func (c *IfColumnNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
|
||||
return dialect.ColumnCheckSql(c.TableName, c.ColumnName)
|
||||
}
|
||||
|
||||
@@ -29,10 +29,12 @@ type Dialect interface {
|
||||
DropTable(tableName string) string
|
||||
DropIndexSql(tableName string, index *Index) string
|
||||
|
||||
TableCheckSql(tableName string) (string, []interface{})
|
||||
RenameTable(oldName string, newName string) string
|
||||
UpdateTableSql(tableName string, columns []*Column) string
|
||||
|
||||
IndexCheckSql(tableName, indexName string) (string, []interface{})
|
||||
ColumnCheckSql(tableName, columnName string) (string, []interface{})
|
||||
|
||||
ColString(*Column) string
|
||||
ColStringNoPk(*Column) string
|
||||
|
||||
@@ -182,6 +184,10 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
|
||||
return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", quote(oldName), quote(newName))
|
||||
}
|
||||
|
||||
func (db *BaseDialect) ColumnCheckSql(tableName, columnName string) (string, []interface{}) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
|
||||
quote := db.dialect.Quote
|
||||
name := index.XName(tableName)
|
||||
|
||||
@@ -85,7 +85,9 @@ type AddColumnMigration struct {
|
||||
}
|
||||
|
||||
func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration {
|
||||
return &AddColumnMigration{tableName: table.Name, column: col}
|
||||
m := &AddColumnMigration{tableName: table.Name, column: col}
|
||||
m.Condition = &IfColumnNotExistsCondition{TableName: table.Name, ColumnName: col.Name}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
|
||||
@@ -109,7 +111,9 @@ type AddIndexMigration struct {
|
||||
}
|
||||
|
||||
func NewAddIndexMigration(table Table, index *Index) *AddIndexMigration {
|
||||
return &AddIndexMigration{tableName: table.Name, index: index}
|
||||
m := &AddIndexMigration{tableName: table.Name, index: index}
|
||||
m.Condition = &IfIndexNotExistsCondition{TableName: table.Name, IndexName: index.XName(table.Name)}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration {
|
||||
@@ -128,7 +132,9 @@ type DropIndexMigration struct {
|
||||
}
|
||||
|
||||
func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
|
||||
return &DropIndexMigration{tableName: table.Name, index: index}
|
||||
m := &DropIndexMigration{tableName: table.Name, index: index}
|
||||
m.Condition = &IfIndexExistsCondition{TableName: table.Name, IndexName: index.XName(table.Name)}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *DropIndexMigration) Sql(dialect Dialect) string {
|
||||
@@ -179,11 +185,6 @@ func NewRenameTableMigration(oldName string, newName string) *RenameTableMigrati
|
||||
return &RenameTableMigration{oldName: oldName, newName: newName}
|
||||
}
|
||||
|
||||
func (m *RenameTableMigration) IfTableExists(tableName string) *RenameTableMigration {
|
||||
m.Condition = &IfTableExistsCondition{TableName: tableName}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *RenameTableMigration) Rename(oldName string, newName string) *RenameTableMigration {
|
||||
m.oldName = oldName
|
||||
m.newName = newName
|
||||
@@ -212,11 +213,6 @@ func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap ma
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *CopyTableDataMigration) IfTableExists(tableName string) *CopyTableDataMigration {
|
||||
m.Condition = &IfTableExistsCondition{TableName: tableName}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *CopyTableDataMigration) Sql(d Dialect) string {
|
||||
return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols)
|
||||
}
|
||||
|
||||
@@ -94,8 +94,6 @@ func (mg *Migrator) Start() error {
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
mg.Logger.Debug("Executing", "sql", sql)
|
||||
|
||||
err := mg.inTransaction(func(sess *xorm.Session) error {
|
||||
err := mg.exec(m, sess)
|
||||
if err != nil {
|
||||
@@ -123,18 +121,30 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
|
||||
condition := m.GetCondition()
|
||||
if condition != nil {
|
||||
sql, args := condition.Sql(mg.Dialect)
|
||||
results, err := sess.SQL(sql).Query(args...)
|
||||
if err != nil || len(results) == 0 {
|
||||
mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
|
||||
return sess.Rollback()
|
||||
|
||||
if sql != "" {
|
||||
mg.Logger.Debug("Executing migration condition sql", "id", m.Id(), "sql", sql, "args", args)
|
||||
results, err := sess.SQL(sql, args...).Query()
|
||||
if err != nil {
|
||||
mg.Logger.Error("Executing migration condition failed", "id", m.Id(), "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !condition.IsFulfilled(results) {
|
||||
mg.Logger.Warn("Skipping migration: Already executed, but not recorded in migration log", "id", m.Id())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if codeMigration, ok := m.(CodeMigration); ok {
|
||||
mg.Logger.Debug("Executing code migration", "id", m.Id())
|
||||
err = codeMigration.Exec(sess, mg)
|
||||
} else {
|
||||
_, err = sess.Exec(m.Sql(mg.Dialect))
|
||||
sql := m.Sql(mg.Dialect)
|
||||
mg.Logger.Debug("Executing sql migration", "id", m.Id(), "sql", sql)
|
||||
_, err = sess.Exec(sql)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -90,12 +90,6 @@ func (db *Mysql) SqlType(c *Column) string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) {
|
||||
args := []interface{}{"grafana", tableName}
|
||||
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
var statements = []string{}
|
||||
|
||||
@@ -108,6 +102,18 @@ func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
|
||||
}
|
||||
|
||||
func (db *Mysql) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
|
||||
args := []interface{}{tableName, indexName}
|
||||
sql := "SELECT 1 FROM " + db.Quote("INFORMATION_SCHEMA") + "." + db.Quote("STATISTICS") + " WHERE " + db.Quote("TABLE_SCHEMA") + " = DATABASE() AND " + db.Quote("TABLE_NAME") + "=? AND " + db.Quote("INDEX_NAME") + "=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func (db *Mysql) ColumnCheckSql(tableName, columnName string) (string, []interface{}) {
|
||||
args := []interface{}{tableName, columnName}
|
||||
sql := "SELECT 1 FROM " + db.Quote("INFORMATION_SCHEMA") + "." + db.Quote("COLUMNS") + " WHERE " + db.Quote("TABLE_SCHEMA") + " = DATABASE() AND " + db.Quote("TABLE_NAME") + "=? AND " + db.Quote("COLUMN_NAME") + "=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func (db *Mysql) CleanDB() error {
|
||||
tables, _ := db.engine.DBMetas()
|
||||
sess := db.engine.NewSession()
|
||||
|
||||
@@ -101,9 +101,9 @@ func (db *Postgres) SqlType(c *Column) string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) {
|
||||
args := []interface{}{"grafana", tableName}
|
||||
sql := "SELECT table_name FROM information_schema.tables WHERE table_schema=? and table_name=?"
|
||||
func (db *Postgres) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
|
||||
args := []interface{}{tableName, indexName}
|
||||
sql := "SELECT 1 FROM " + db.Quote("pg_indexes") + " WHERE" + db.Quote("tablename") + "=? AND " + db.Quote("indexname") + "=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
|
||||
@@ -68,9 +68,10 @@ func (db *Sqlite3) SqlType(c *Column) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) {
|
||||
args := []interface{}{tableName}
|
||||
return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
|
||||
func (db *Sqlite3) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
|
||||
args := []interface{}{tableName, indexName}
|
||||
sql := "SELECT 1 FROM " + db.Quote("sqlite_master") + " WHERE " + db.Quote("type") + "='index' AND " + db.Quote("tbl_name") + "=? AND " + db.Quote("name") + "=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string {
|
||||
|
||||
@@ -243,7 +243,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
|
||||
ss.dbCfg.Path = filepath.Join(ss.Cfg.DataPath, ss.dbCfg.Path)
|
||||
}
|
||||
os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm)
|
||||
cnnstr = "file:" + ss.dbCfg.Path + "?cache=shared&mode=rwc"
|
||||
cnnstr = fmt.Sprintf("file:%s?cache=%s&mode=rwc", ss.dbCfg.Path, ss.dbCfg.CacheMode)
|
||||
default:
|
||||
return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type)
|
||||
}
|
||||
@@ -319,6 +319,8 @@ func (ss *SqlStore) readConfig() {
|
||||
ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String()
|
||||
ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String()
|
||||
ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db")
|
||||
|
||||
ss.dbCfg.CacheMode = sec.Key("cache_mode").MustString("private")
|
||||
}
|
||||
|
||||
func InitTestDB(t *testing.T) *SqlStore {
|
||||
@@ -391,13 +393,20 @@ func IsTestDbPostgres() bool {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type, Host, Name, User, Pwd, Path, SslMode string
|
||||
CaCertPath string
|
||||
ClientKeyPath string
|
||||
ClientCertPath string
|
||||
ServerCertName string
|
||||
ConnectionString string
|
||||
MaxOpenConn int
|
||||
MaxIdleConn int
|
||||
ConnMaxLifetime int
|
||||
Type string
|
||||
Host string
|
||||
Name string
|
||||
User string
|
||||
Pwd string
|
||||
Path string
|
||||
SslMode string
|
||||
CaCertPath string
|
||||
ClientKeyPath string
|
||||
ClientCertPath string
|
||||
ServerCertName string
|
||||
ConnectionString string
|
||||
MaxOpenConn int
|
||||
MaxIdleConn int
|
||||
ConnMaxLifetime int
|
||||
CacheMode string
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data
|
||||
rawQuery := model.Get("query").MustString("")
|
||||
useRawQuery := model.Get("rawQuery").MustBool(false)
|
||||
alias := model.Get("alias").MustString("")
|
||||
tz := model.Get("tz").MustString("")
|
||||
|
||||
measurement := model.Get("measurement").MustString("")
|
||||
|
||||
@@ -55,6 +56,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data
|
||||
Interval: parsedInterval,
|
||||
Alias: alias,
|
||||
UseRawQuery: useRawQuery,
|
||||
Tz: tz,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ func TestInfluxdbQueryParser(t *testing.T) {
|
||||
}
|
||||
],
|
||||
"measurement": "logins.count",
|
||||
"tz": "Europe/Paris",
|
||||
"policy": "default",
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
@@ -115,6 +116,7 @@ func TestInfluxdbQueryParser(t *testing.T) {
|
||||
So(len(res.GroupBy), ShouldEqual, 3)
|
||||
So(len(res.Selects), ShouldEqual, 3)
|
||||
So(len(res.Tags), ShouldEqual, 2)
|
||||
So(res.Tz, ShouldEqual, "Europe/Paris")
|
||||
So(res.Interval, ShouldEqual, time.Second*20)
|
||||
So(res.Alias, ShouldEqual, "serie alias")
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ type Query struct {
|
||||
UseRawQuery bool
|
||||
Alias string
|
||||
Interval time.Duration
|
||||
Tz string
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
|
||||
@@ -26,6 +26,7 @@ func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) {
|
||||
res += query.renderWhereClause()
|
||||
res += query.renderTimeFilter(queryContext)
|
||||
res += query.renderGroupBy(queryContext)
|
||||
res += query.renderTz()
|
||||
}
|
||||
|
||||
calculator := tsdb.NewIntervalCalculator(&tsdb.IntervalOptions{})
|
||||
@@ -154,3 +155,12 @@ func (query *Query) renderGroupBy(queryContext *tsdb.TsdbQuery) string {
|
||||
|
||||
return groupBy
|
||||
}
|
||||
|
||||
func (query *Query) renderTz() string {
|
||||
tz := query.Tz
|
||||
if tz == "" {
|
||||
return ""
|
||||
} else {
|
||||
return fmt.Sprintf(" tz('%s')", tz)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,20 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
|
||||
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`)
|
||||
})
|
||||
|
||||
Convey("can build query with tz", func() {
|
||||
query := &Query{
|
||||
Selects: []*Select{{*qp1, *qp2}},
|
||||
Measurement: "cpu",
|
||||
GroupBy: []*QueryPart{groupBy1},
|
||||
Tz: "Europe/Paris",
|
||||
Interval: time.Second * 5,
|
||||
}
|
||||
|
||||
rawQuery, err := query.Build(queryContext)
|
||||
So(err, ShouldBeNil)
|
||||
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(5s) tz('Europe/Paris')`)
|
||||
})
|
||||
|
||||
Convey("can build query with group bys", func() {
|
||||
query := &Query{
|
||||
Selects: []*Select{{*qp1, *qp2}},
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { PureComponent, ReactNode, ReactElement } from 'react';
|
||||
import { Label } from './Label';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
interface Props {
|
||||
label?: ReactNode;
|
||||
labelClassName?: string;
|
||||
id?: string;
|
||||
children: ReactElement<any>;
|
||||
}
|
||||
|
||||
export class Element extends PureComponent<Props> {
|
||||
elementId: string = this.props.id || uniqueId('form-element-');
|
||||
|
||||
get elementLabel() {
|
||||
const { label, labelClassName } = this.props;
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<Label htmlFor={this.elementId} className={labelClassName}>
|
||||
{label}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get children() {
|
||||
const { children } = this.props;
|
||||
|
||||
return React.cloneElement(children, { id: this.elementId });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="our-custom-wrapper-class">
|
||||
{this.elementLabel}
|
||||
{this.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
htmlFor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class Label extends PureComponent<Props> {
|
||||
render() {
|
||||
const { children, htmlFor, className } = this.props;
|
||||
|
||||
return (
|
||||
<label className={`custom-label-class ${className || ''}`} htmlFor={htmlFor}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1 @@
|
||||
export { Element } from './Element';
|
||||
export { Input } from './Input';
|
||||
export { Label } from './Label';
|
||||
|
||||
@@ -52,7 +52,11 @@ export const ToggleButton: SFC<ToggleButtonProps> = ({
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return <Tooltip content={tooltip}>{button}</Tooltip>;
|
||||
return (
|
||||
<Tooltip content={tooltip} placement="bottom">
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ function bootstrapTagsinput() {
|
||||
},
|
||||
});
|
||||
|
||||
select.on('itemAdded', event => {
|
||||
select.on('itemAdded', (event: any) => {
|
||||
if (scope.model.indexOf(event.item) === -1) {
|
||||
scope.model.push(event.item);
|
||||
if (scope.onTagsUpdated) {
|
||||
@@ -85,7 +85,7 @@ function bootstrapTagsinput() {
|
||||
setColor(event.item, tagElement);
|
||||
});
|
||||
|
||||
select.on('itemRemoved', event => {
|
||||
select.on('itemRemoved', (event: any) => {
|
||||
const idx = scope.model.indexOf(event.item);
|
||||
if (idx !== -1) {
|
||||
scope.model.splice(idx, 1);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class LiveSrv {
|
||||
conn: any;
|
||||
|
||||
@@ -2,14 +2,23 @@ import _ from 'lodash';
|
||||
import { TimeSeries } from 'app/core/core';
|
||||
import colors, { getThemeColor } from 'app/core/utils/colors';
|
||||
|
||||
/**
|
||||
* Mapping of log level abbreviation to canonical log level.
|
||||
* Supported levels are reduce to limit color variation.
|
||||
*/
|
||||
export enum LogLevel {
|
||||
emerg = 'critical',
|
||||
alert = 'critical',
|
||||
crit = 'critical',
|
||||
critical = 'critical',
|
||||
warn = 'warning',
|
||||
warning = 'warning',
|
||||
err = 'error',
|
||||
eror = 'error',
|
||||
error = 'error',
|
||||
info = 'info',
|
||||
notice = 'info',
|
||||
dbug = 'debug',
|
||||
debug = 'debug',
|
||||
trace = 'trace',
|
||||
unkown = 'unkown',
|
||||
@@ -81,7 +90,9 @@ export interface LogsStream {
|
||||
|
||||
export interface LogsStreamEntry {
|
||||
line: string;
|
||||
timestamp: string;
|
||||
ts: string;
|
||||
// Legacy, was renamed to ts
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface LogsStreamLabels {
|
||||
|
||||
@@ -14,7 +14,6 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||
datasourceError: null,
|
||||
datasourceLoading: null,
|
||||
datasourceMissing: false,
|
||||
datasourceName: '',
|
||||
exploreDatasources: [],
|
||||
graphInterval: 1000,
|
||||
history: [],
|
||||
@@ -69,7 +68,7 @@ describe('state functions', () => {
|
||||
it('returns url parameter value for a state object', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
datasourceName: 'foo',
|
||||
initialDatasource: 'foo',
|
||||
range: {
|
||||
from: 'now-5h',
|
||||
to: 'now',
|
||||
@@ -94,7 +93,7 @@ describe('state functions', () => {
|
||||
it('returns url parameter value for a state object', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
datasourceName: 'foo',
|
||||
initialDatasource: 'foo',
|
||||
range: {
|
||||
from: 'now-5h',
|
||||
to: 'now',
|
||||
@@ -120,7 +119,7 @@ describe('state functions', () => {
|
||||
it('can parse the serialized state into the original state', () => {
|
||||
const state = {
|
||||
...DEFAULT_EXPLORE_STATE,
|
||||
datasourceName: 'foo',
|
||||
initialDatasource: 'foo',
|
||||
range: {
|
||||
from: 'now - 5h',
|
||||
to: 'now',
|
||||
@@ -144,7 +143,7 @@ describe('state functions', () => {
|
||||
const resultState = {
|
||||
...rest,
|
||||
datasource: DEFAULT_EXPLORE_STATE.datasource,
|
||||
datasourceName: datasource,
|
||||
initialDatasource: datasource,
|
||||
initialQueries: queries,
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import { parse as parseDate } from 'app/core/utils/datemath';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
|
||||
import { DataQuery, RawTimeRange, IntervalValues, DataSourceApi } from 'app/types/series';
|
||||
import { DataQuery, DataSourceApi } from 'app/types/series';
|
||||
import { RawTimeRange, IntervalValues } from '@grafana/ui';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
@@ -104,7 +105,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
|
||||
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
|
||||
const urlState: ExploreUrlState = {
|
||||
datasource: state.datasourceName,
|
||||
datasource: state.initialDatasource,
|
||||
queries: state.initialQueries.map(clearQueryKeys),
|
||||
range: state.range,
|
||||
};
|
||||
|
||||
@@ -629,6 +629,8 @@ kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³');
|
||||
kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³');
|
||||
kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³');
|
||||
kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³');
|
||||
kbn.valueFormats.conmgdL = kbn.formatBuilders.fixedUnit('mg/dL');
|
||||
kbn.valueFormats.conmmolL = kbn.formatBuilders.fixedUnit('mmol/L');
|
||||
|
||||
// Time
|
||||
kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
|
||||
@@ -1209,6 +1211,8 @@ kbn.getUnitFormats = () => {
|
||||
{ text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' },
|
||||
{ text: 'gram per cubic meter (g/m³)', value: 'congm3' },
|
||||
{ text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' },
|
||||
{ text: 'milligrams per decilitre (mg/dL)', value: 'conmgdL' },
|
||||
{ text: 'millimoles per litre (mmol/L)', value: 'conmmolL' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
|
||||
import * as dateMath from './datemath';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import classNames from 'classnames/bind';
|
||||
import classNames from 'classnames';
|
||||
import { AlertRule } from '../../types';
|
||||
|
||||
export interface Props {
|
||||
@@ -23,7 +23,7 @@ class AlertRuleItem extends PureComponent<Props> {
|
||||
render() {
|
||||
const { rule, onTogglePause } = this.props;
|
||||
|
||||
const stateClass = classNames({
|
||||
const iconClassName = classNames({
|
||||
fa: true,
|
||||
'fa-play': rule.state === 'paused',
|
||||
'fa-pause': rule.state !== 'paused',
|
||||
@@ -55,7 +55,7 @@ class AlertRuleItem extends PureComponent<Props> {
|
||||
title="Pausing an alert rule prevents it from executing"
|
||||
onClick={onTogglePause}
|
||||
>
|
||||
<i className={stateClass} />
|
||||
<i className={iconClassName} />
|
||||
</button>
|
||||
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule">
|
||||
<i className="icon-gf icon-gf-settings" />
|
||||
|
||||
@@ -13,7 +13,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@@ -224,7 +224,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
<td>{key.name}</td>
|
||||
<td>{key.role}</td>
|
||||
<td>
|
||||
<DeleteButton onConfirmDelete={() => this.onDeleteApiKey(key)} />
|
||||
<DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -160,14 +160,14 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<PanelResizer
|
||||
isEditing={!!isEditing}
|
||||
isEditing={isEditing}
|
||||
panel={panel}
|
||||
render={(panelHeight: number | 'inherit') => (
|
||||
render={styles => (
|
||||
<div
|
||||
className={panelWrapperClass}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
style={{ height: panelHeight }}
|
||||
style={styles}
|
||||
>
|
||||
{plugin.exports.Panel && this.renderReactPanel()}
|
||||
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
// Types
|
||||
import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types';
|
||||
import { DataQueryOptions, DataQueryResponse } from 'app/types';
|
||||
import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui';
|
||||
|
||||
interface RenderProps {
|
||||
loading: LoadingState;
|
||||
|
||||
@@ -16,7 +16,8 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelPlugin, TimeRange } from 'app/types';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
|
||||
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
||||
import { PanelMenuItem } from 'app/types/panel';
|
||||
import { PanelMenuItem } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { SFC } from 'react';
|
||||
import { PanelMenuItem } from 'app/types/panel';
|
||||
import { PanelMenuItem } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
children: any;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelPlugin, PanelProps } from 'app/types';
|
||||
|
||||
// Types
|
||||
import { PanelProps } from '@grafana/ui';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { throttle } from 'lodash';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PanelModel } from '../panel_model';
|
||||
|
||||
interface Props {
|
||||
isEditing: boolean;
|
||||
render: (height: number | 'inherit') => JSX.Element;
|
||||
render: (styles: object) => JSX.Element;
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export class PanelResizer extends PureComponent<Props, State> {
|
||||
prevEditorHeight: number;
|
||||
throttledChangeHeight: (height: number) => void;
|
||||
throttledResizeDone: () => void;
|
||||
noStyles: object = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -65,7 +66,7 @@ export class PanelResizer extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{render(isEditing ? editorHeight : 'inherit')}
|
||||
{render(isEditing ? {height: editorHeight} : this.noStyles)}
|
||||
{isEditing && (
|
||||
<div className="panel-editor-container__resizer">
|
||||
<Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Input } from 'app/core/components/Form';
|
||||
import { EventsWithValidation } from 'app/core/components/Form/Input';
|
||||
import { InputStatus } from 'app/core/components/Form/Input';
|
||||
import DataSourceOption from './DataSourceOption';
|
||||
import { GfFormLabel } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
@@ -163,7 +164,7 @@ export class QueryOptions extends PureComponent<Props, State> {
|
||||
{this.renderOptions()}
|
||||
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label">Relative time</span>
|
||||
<GfFormLabel>Relative time</GfFormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
className="width-6"
|
||||
|
||||
@@ -6,6 +6,7 @@ function dashLinksContainer() {
|
||||
return {
|
||||
scope: {
|
||||
links: '=',
|
||||
dashboard: '=',
|
||||
},
|
||||
restrict: 'E',
|
||||
controller: 'DashLinksContainerCtrl',
|
||||
@@ -20,6 +21,8 @@ function dashLink($compile, $sanitize, linkSrv) {
|
||||
restrict: 'E',
|
||||
link: (scope, elem) => {
|
||||
const link = scope.link;
|
||||
const dashboard = scope.dashboard;
|
||||
|
||||
let template =
|
||||
'<div class="gf-form">' +
|
||||
'<a class="pointer gf-form-label" data-placement="bottom"' +
|
||||
@@ -76,7 +79,7 @@ function dashLink($compile, $sanitize, linkSrv) {
|
||||
}
|
||||
|
||||
update();
|
||||
scope.$on('refresh', update);
|
||||
dashboard.events.on('refresh', update, scope);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.dashboard.links.length > 0" >
|
||||
<dash-links-container links="ctrl.dashboard.links" class="gf-form-inline"></dash-links-container>
|
||||
<dash-links-container links="ctrl.dashboard.links" dashboard="ctrl.dashboard" class="gf-form-inline"></dash-links-container>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@@ -6,9 +6,9 @@ import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
// Types
|
||||
|
||||
import { TimeRange } from 'app/types';
|
||||
// Types
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
|
||||
export class TimeSrv {
|
||||
time: any;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { store } from 'app/store/store';
|
||||
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelMenuItem } from 'app/types/panel';
|
||||
import { PanelMenuItem } from '@grafana/ui';
|
||||
|
||||
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
const onViewPanel = () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import store from 'app/core/store';
|
||||
// Models
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import { TimeRange } from 'app/types/series';
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
|
||||
// Utils
|
||||
import { isString as _isString } from 'lodash';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import classNames from 'classnames';
|
||||
import DataSourcesListItem from './DataSourcesListItem';
|
||||
import { DataSource } from 'app/types';
|
||||
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
QueryHintGetter,
|
||||
QueryHint,
|
||||
} from 'app/types/explore';
|
||||
import { TimeRange, DataQuery } from 'app/types/series';
|
||||
import { TimeRange } from '@grafana/ui';
|
||||
import { DataQuery } from 'app/types/series';
|
||||
import store from 'app/core/store';
|
||||
import {
|
||||
DEFAULT_RANGE,
|
||||
@@ -39,6 +40,8 @@ import ErrorBoundary from './ErrorBoundary';
|
||||
import { Alert } from './Error';
|
||||
import TimePicker, { parseTime } from './TimePicker';
|
||||
|
||||
const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
|
||||
|
||||
interface ExploreProps {
|
||||
datasourceSrv: DatasourceSrv;
|
||||
onChangeSplit: (split: boolean, state?: ExploreState) => void;
|
||||
@@ -89,6 +92,10 @@ interface ExploreProps {
|
||||
export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
el: any;
|
||||
exploreEvents: Emitter;
|
||||
/**
|
||||
* Set via URL or local storage
|
||||
*/
|
||||
initialDatasource: string;
|
||||
/**
|
||||
* Current query expressions of the rows including their modifications, used for running queries.
|
||||
* Not kept in component state to prevent edit-render roundtrips.
|
||||
@@ -114,6 +121,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
initialQueries = splitState.initialQueries;
|
||||
} else {
|
||||
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
||||
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
||||
initialQueries = ensureQueries(queries);
|
||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
|
||||
// Millies step for helper bar charts
|
||||
@@ -123,10 +131,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
datasourceError: null,
|
||||
datasourceLoading: null,
|
||||
datasourceMissing: false,
|
||||
datasourceName: datasource,
|
||||
exploreDatasources: [],
|
||||
graphInterval: initialGraphInterval,
|
||||
graphResult: [],
|
||||
initialDatasource,
|
||||
initialQueries,
|
||||
history: [],
|
||||
logsResult: null,
|
||||
@@ -150,7 +158,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
|
||||
async componentDidMount() {
|
||||
const { datasourceSrv } = this.props;
|
||||
const { datasourceName } = this.state;
|
||||
const { initialDatasource } = this.state;
|
||||
if (!datasourceSrv) {
|
||||
throw new Error('No datasource service passed as props.');
|
||||
}
|
||||
@@ -164,10 +172,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
|
||||
if (datasources.length > 0) {
|
||||
this.setState({ datasourceLoading: true, exploreDatasources });
|
||||
// Priority: datasource in url, default datasource, first explore datasource
|
||||
// Priority for datasource preselection: URL, localstorage, default datasource
|
||||
let datasource;
|
||||
if (datasourceName) {
|
||||
datasource = await datasourceSrv.get(datasourceName);
|
||||
if (initialDatasource) {
|
||||
datasource = await datasourceSrv.get(initialDatasource);
|
||||
} else {
|
||||
datasource = await datasourceSrv.get();
|
||||
}
|
||||
@@ -252,13 +260,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
datasourceLoading: false,
|
||||
datasourceName: datasource.name,
|
||||
initialDatasource: datasource.name,
|
||||
initialQueries: nextQueries,
|
||||
logsHighlighterExpressions: undefined,
|
||||
showingStartPage: Boolean(StartPage),
|
||||
},
|
||||
() => {
|
||||
if (datasourceError === null) {
|
||||
// Save last-used datasource
|
||||
store.set(LAST_USED_DATASOURCE_KEY, datasource.name);
|
||||
this.onSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'vendor/flot/jquery.flot.time';
|
||||
import 'vendor/flot/jquery.flot.selection';
|
||||
import 'vendor/flot/jquery.flot.stack';
|
||||
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Highlighter from 'react-highlight-words';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
import {
|
||||
LogsDedupDescription,
|
||||
LogsDedupStrategy,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { getIntervals } from 'app/core/utils/explore';
|
||||
import { DataQuery } from 'app/types';
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
import { getTimeSrv } from 'app/features/dashboard/time_srv';
|
||||
import 'app/features/plugins/plugin_loader';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
|
||||
import { Change, Value } from 'slate';
|
||||
import { Editor } from 'slate-react';
|
||||
import Plain from 'slate-plain-serializer';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
|
||||
|
||||
@@ -30,6 +31,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
|
||||
export interface QueryFieldProps {
|
||||
additionalPlugins?: any[];
|
||||
cleanText?: (text: string) => string;
|
||||
disabled?: boolean;
|
||||
initialQuery: string | null;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
@@ -78,7 +80,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || '');
|
||||
|
||||
// Base plugins
|
||||
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p);
|
||||
this.plugins = [ClearPlugin(), NewlinePlugin(), ...(props.additionalPlugins || [])].filter(p => p);
|
||||
|
||||
this.state = {
|
||||
suggestions: [],
|
||||
@@ -440,12 +442,17 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
};
|
||||
|
||||
render() {
|
||||
const { disabled } = this.props;
|
||||
const wrapperClassName = classnames('slate-query-field__wrapper', {
|
||||
'slate-query-field__wrapper--disabled': disabled,
|
||||
});
|
||||
return (
|
||||
<div className="slate-query-field-wrapper">
|
||||
<div className={wrapperClassName}>
|
||||
<div className="slate-query-field">
|
||||
{this.renderMenu()}
|
||||
<Editor
|
||||
autoCorrect={false}
|
||||
readOnly={this.props.disabled}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Emitter } from 'app/core/utils/emitter';
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryTransactionStatus from './QueryTransactionStatus';
|
||||
import { DataSource, DataQuery } from 'app/types';
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { RawTimeRange } from '@grafana/ui';
|
||||
|
||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||
|
||||
@@ -3,7 +3,7 @@ import moment from 'moment';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||
import { RawTimeRange, TimeRange } from 'app/types/series';
|
||||
import { RawTimeRange, TimeRange } from '@grafana/ui';
|
||||
|
||||
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
export const DEFAULT_RANGE = {
|
||||
|
||||
@@ -198,11 +198,10 @@ export class PanelCtrl {
|
||||
}
|
||||
|
||||
calculatePanelHeight() {
|
||||
if (this.panel.fullscreen) {
|
||||
const docHeight = $('.react-grid-layout').height();
|
||||
const editHeight = Math.floor(docHeight * 0.35);
|
||||
const fullscreenHeight = Math.floor(docHeight * 0.8);
|
||||
this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight;
|
||||
if (this.panel.isEditing) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="panel-option-section__body">
|
||||
<div class="section">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Repat</span>
|
||||
<span class="gf-form-label width-9">Repeat</span>
|
||||
<dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.repeat">
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__header">Drildown Links</div>
|
||||
<div class="panel-option-section__header">Drilldown Links</div>
|
||||
<div class="panel-option-section__body">
|
||||
<panel-links-editor panel="ctrl.panel"></panel-links-editor>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { SFC } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import classNames from 'classnames';
|
||||
import PluginListItem from './PluginListItem';
|
||||
import { Plugin } from 'app/types';
|
||||
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
@@ -26,16 +26,10 @@ import * as ticks from 'app/core/utils/ticks';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import builtInPlugins from './built_in_plugins';
|
||||
import * as d3 from 'd3';
|
||||
import * as grafanaUI from '@grafana/ui';
|
||||
|
||||
// rxjs
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
// these imports add functions to Observable
|
||||
import 'rxjs/add/observable/empty';
|
||||
import 'rxjs/add/observable/from';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/combineAll';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
// add cache busting
|
||||
const bust = `?_cache=${Date.now()}`;
|
||||
@@ -71,6 +65,7 @@ function exposeToPlugin(name: string, component: any) {
|
||||
});
|
||||
}
|
||||
|
||||
exposeToPlugin('@grafana/ui', grafanaUI);
|
||||
exposeToPlugin('lodash', _);
|
||||
exposeToPlugin('moment', moment);
|
||||
exposeToPlugin('jquery', jquery);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { NavModel, Team } from '../../types';
|
||||
@@ -58,7 +58,7 @@ export class TeamList extends PureComponent<Props, any> {
|
||||
<a href={teamUrl}>{team.memberCount}</a>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirmDelete={() => this.deleteTeam(team)} />
|
||||
<DeleteButton onConfirm={() => this.deleteTeam(team)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { TeamMember, User } from 'app/types';
|
||||
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
|
||||
@@ -76,7 +76,7 @@ export class TeamMembers extends PureComponent<Props, State> {
|
||||
<td>{member.email}</td>
|
||||
{syncEnabled && this.renderLabels(member.labels)}
|
||||
<td className="text-right">
|
||||
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
|
||||
<DeleteButton onConfirm={() => this.onRemoveMember(member)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -124,7 +124,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -174,7 +174,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -224,7 +224,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -274,7 +274,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -324,7 +324,7 @@ exports[`Render should render teams table 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -204,7 +204,7 @@ exports[`Render should render team members 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -229,7 +229,7 @@ exports[`Render should render team members 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -254,7 +254,7 @@ exports[`Render should render team members 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -279,7 +279,7 @@ exports[`Render should render team members 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -304,7 +304,7 @@ exports[`Render should render team members 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -441,7 +441,7 @@ exports[`Render should render team members when sync enabled 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -482,7 +482,7 @@ exports[`Render should render team members when sync enabled 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -523,7 +523,7 @@ exports[`Render should render team members when sync enabled 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -564,7 +564,7 @@ exports[`Render should render team members when sync enabled 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -605,7 +605,7 @@ exports[`Render should render team members when sync enabled 1`] = `
|
||||
className="text-right"
|
||||
>
|
||||
<DeleteButton
|
||||
onConfirmDelete={[Function]}
|
||||
onConfirm={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames/bind';
|
||||
import classNames from 'classnames';
|
||||
import { setUsersSearchQuery } from './state/actions';
|
||||
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
|
||||
|
||||
|
||||
@@ -256,6 +256,10 @@ export default class InfluxQuery {
|
||||
query += ' SLIMIT ' + target.slimit;
|
||||
}
|
||||
|
||||
if (target.tz) {
|
||||
query += " tz('" + target.tz + "')";
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.target.tz">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">tz</label>
|
||||
<input type="text" class="gf-form-input width-9" ng-model="ctrl.target.tz" spellcheck='false' placeholder="No Timezone" ng-blur="ctrl.refresh()">
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">FORMAT AS</label>
|
||||
|
||||
@@ -100,6 +100,9 @@ export class InfluxQueryCtrl extends QueryCtrl {
|
||||
if (!this.target.slimit) {
|
||||
options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' }));
|
||||
}
|
||||
if (!this.target.tz) {
|
||||
options.push(this.uiSegmentSrv.newSegment({ value: 'tz' }));
|
||||
}
|
||||
if (this.target.orderByTime === 'ASC') {
|
||||
options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' }));
|
||||
}
|
||||
@@ -124,6 +127,10 @@ export class InfluxQueryCtrl extends QueryCtrl {
|
||||
this.target.slimit = 10;
|
||||
break;
|
||||
}
|
||||
case 'tz': {
|
||||
this.target.tz = 'UTC';
|
||||
break;
|
||||
}
|
||||
case 'ORDER BY time DESC': {
|
||||
this.target.orderByTime = 'DESC';
|
||||
break;
|
||||
|
||||
@@ -2,14 +2,23 @@ import React from 'react';
|
||||
|
||||
const CHEAT_SHEET_ITEMS = [
|
||||
{
|
||||
title: 'Logs From a Job',
|
||||
title: 'See your logs',
|
||||
label: 'Start by selecting a log stream from the Log labels selector.',
|
||||
},
|
||||
{
|
||||
title: 'Logs from a "job"',
|
||||
expression: '{job="default/prometheus"}',
|
||||
label: 'Returns all log lines emitted by instances of this job.',
|
||||
},
|
||||
{
|
||||
title: 'Search For Text',
|
||||
expression: '{app="cassandra"} Maximum memory usage',
|
||||
label: 'Returns all log lines for the selector and highlights the given text in the results.',
|
||||
title: 'Combine stream selectors',
|
||||
expression: '{app="cassandra",namespace="prod"}',
|
||||
label: 'Returns all log lines from streams that have both labels.',
|
||||
},
|
||||
{
|
||||
title: 'Search for text',
|
||||
expression: '{app="cassandra"} (duration|latency)\\s*(=|is|of)\\s*[\\d\\.]+',
|
||||
label: 'Add a regular expression after the selector to filter for.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -19,12 +28,14 @@ export default (props: any) => (
|
||||
{CHEAT_SHEET_ITEMS.map(item => (
|
||||
<div className="cheat-sheet-item" key={item.expression}>
|
||||
<div className="cheat-sheet-item__title">{item.title}</div>
|
||||
<div
|
||||
className="cheat-sheet-item__expression"
|
||||
onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
|
||||
>
|
||||
<code>{item.expression}</code>
|
||||
</div>
|
||||
{item.expression && (
|
||||
<div
|
||||
className="cheat-sheet-item__expression"
|
||||
onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
|
||||
>
|
||||
<code>{item.expression}</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="cheat-sheet-item__label">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -15,6 +15,16 @@ import { DataQuery } from 'app/types';
|
||||
|
||||
const PRISM_SYNTAX = 'promql';
|
||||
|
||||
function getChooserText(hasSytax, hasLogLabels) {
|
||||
if (!hasSytax) {
|
||||
return 'Loading labels...';
|
||||
}
|
||||
if (!hasLogLabels) {
|
||||
return '(No labels found)';
|
||||
}
|
||||
return 'Log labels';
|
||||
}
|
||||
|
||||
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
|
||||
// Modify suggestion based on context
|
||||
switch (typeaheadContext) {
|
||||
@@ -67,7 +77,10 @@ interface LokiQueryFieldState {
|
||||
|
||||
class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
|
||||
plugins: any[];
|
||||
pluginsSearch: any[];
|
||||
languageProvider: any;
|
||||
modifiedSearch: string;
|
||||
modifiedQuery: string;
|
||||
|
||||
constructor(props: LokiQueryFieldProps, context) {
|
||||
super(props, context);
|
||||
@@ -85,6 +98,8 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
}),
|
||||
];
|
||||
|
||||
this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
|
||||
|
||||
this.state = {
|
||||
logLabelOptions: [],
|
||||
syntaxLoaded: false,
|
||||
@@ -189,7 +204,8 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
const { error, hint, initialQuery } = this.props;
|
||||
const { logLabelOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
|
||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
|
||||
|
||||
return (
|
||||
<div className="prom-query-field">
|
||||
@@ -208,7 +224,7 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
onTypeahead={this.onTypeahead}
|
||||
onWillApplySuggestion={willApplySuggestion}
|
||||
onValueChanged={this.onChangeQuery}
|
||||
placeholder="Enter a Loki Log query"
|
||||
placeholder="Enter a Loki query"
|
||||
portalOrigin="loki"
|
||||
syntaxLoaded={syntaxLoaded}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
import LokiDatasource from './datasource';
|
||||
|
||||
describe('LokiDatasource', () => {
|
||||
const instanceSettings = {
|
||||
const instanceSettings: any = {
|
||||
url: 'myloggingurl',
|
||||
};
|
||||
|
||||
describe('when querying', () => {
|
||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||
|
||||
const templateSrvMock = {
|
||||
getAdhocFilters: () => [],
|
||||
replace: a => a,
|
||||
};
|
||||
|
||||
const range = { from: 'now-6h', to: 'now' };
|
||||
|
||||
test('should use default max lines when no limit given', () => {
|
||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn();
|
||||
ds.query({ range, targets: [{ expr: 'foo' }] });
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000');
|
||||
});
|
||||
|
||||
test('should use custom max lines if limit is set', () => {
|
||||
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn();
|
||||
ds.query({ range, targets: [{ expr: 'foo' }] });
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when performing testDataSource', () => {
|
||||
let ds;
|
||||
let result;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user