Merge pull request #5 from grafana/master

Update master
This commit is contained in:
Pavel 2019-01-03 12:33:57 +02:00 committed by GitHub
commit ea4223f923
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
159 changed files with 1216 additions and 691 deletions

View File

@ -16,6 +16,7 @@
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548) * **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) * **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) * **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 ### Bug fixes
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) * **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) * **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) * **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) # 5.4.1 (2018-12-10)

View File

@ -103,6 +103,9 @@ server_cert_name =
# For "sqlite3" only, path relative to data_path setting # For "sqlite3" only, path relative to data_path setting
path = grafana.db path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private
#################################### Session ############################# #################################### Session #############################
[session] [session]
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"

View File

@ -99,6 +99,9 @@
# Set to true to log the sql calls and execution times. # Set to true to log the sql calls and execution times.
log_queries = log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
#################################### Session #################################### #################################### Session ####################################
[session] [session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file" # Either "memory", "file", "redis", "mysql", "postgres", default is "file"

View File

@ -0,0 +1,7 @@
FROM golang:latest
ADD main.go /
WORKDIR /
RUN go build -o main .
EXPOSE 3010
ENTRYPOINT ["/main"]

View File

@ -0,0 +1,5 @@
alert_webhook_listener:
build: docker/blocks/alert_webhook_listener
network_mode: host
ports:
- "3010:3010"

View File

@ -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)
}

View File

@ -31,9 +31,10 @@ auto_sign_up = true
ldap_sync_ttl = 60 ldap_sync_ttl = 60
# Limit where auth proxy requests come from by configuring a list of IP addresses. # 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. # 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 = whitelist =
# Optionally define more headers to sync other user attributes # 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 = headers =
``` ```

View File

@ -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. 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 /> <hr />
## [security] ## [security]

View File

@ -6,7 +6,9 @@ module.exports = {
}, },
"moduleDirectories": ["node_modules", "public"], "moduleDirectories": ["node_modules", "public"],
"roots": [ "roots": [
"<rootDir>/public" "<rootDir>/public/app",
"<rootDir>/public/test",
"<rootDir>/packages"
], ],
"testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$", "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
"moduleFileExtensions": [ "moduleFileExtensions": [

View File

@ -1,4 +1,5 @@
{ {
"private": true,
"author": { "author": {
"name": "Torkel Ödegaard", "name": "Torkel Ödegaard",
"company": "Grafana Labs" "company": "Grafana Labs"
@ -11,14 +12,16 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.1.2", "@babel/core": "^7.1.2",
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.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/d3": "^4.10.1",
"@types/enzyme": "^3.1.13", "@types/enzyme": "^3.1.13",
"@types/jest": "^23.3.2", "@types/jest": "^23.3.2",
"@types/jquery": "^1.10.35",
"@types/node": "^8.0.31", "@types/node": "^8.0.31",
"@types/react": "^16.7.6", "@types/react": "^16.7.6",
"@types/react-custom-scrollbars": "^4.0.5", "@types/react-custom-scrollbars": "^4.0.5",
@ -49,15 +52,12 @@
"grunt-cli": "~1.2.0", "grunt-cli": "~1.2.0",
"grunt-contrib-clean": "~1.0.0", "grunt-contrib-clean": "~1.0.0",
"grunt-contrib-compress": "^1.3.0", "grunt-contrib-compress": "^1.3.0",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "~1.0.0", "grunt-contrib-copy": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.2",
"grunt-exec": "^1.0.1", "grunt-exec": "^1.0.1",
"grunt-newer": "^1.3.0", "grunt-newer": "^1.3.0",
"grunt-notify": "^0.4.5", "grunt-notify": "^0.4.5",
"grunt-postcss": "^0.8.0", "grunt-postcss": "^0.8.0",
"grunt-sass": "^2.0.0", "grunt-sass-lint": "^0.2.4",
"grunt-sass-lint": "^0.2.2",
"grunt-usemin": "3.1.1", "grunt-usemin": "3.1.1",
"grunt-webpack": "^3.0.2", "grunt-webpack": "^3.0.2",
"html-loader": "^0.5.1", "html-loader": "^0.5.1",
@ -73,6 +73,7 @@
"ng-annotate-webpack-plugin": "^0.3.0", "ng-annotate-webpack-plugin": "^0.3.0",
"ngtemplate-loader": "^2.0.1", "ngtemplate-loader": "^2.0.1",
"npm": "^5.4.2", "npm": "^5.4.2",
"node-sass": "^4.11.0",
"optimize-css-assets-webpack-plugin": "^4.0.2", "optimize-css-assets-webpack-plugin": "^4.0.2",
"phantomjs-prebuilt": "^2.1.15", "phantomjs-prebuilt": "^2.1.15",
"postcss-browser-reporter": "^0.5.0", "postcss-browser-reporter": "^0.5.0",
@ -92,6 +93,7 @@
"tslib": "^1.9.3", "tslib": "^1.9.3",
"tslint": "^5.8.0", "tslint": "^5.8.0",
"tslint-loader": "^3.5.3", "tslint-loader": "^3.5.3",
"tslint-react": "^3.6.0",
"typescript": "^3.0.3", "typescript": "^3.0.3",
"uglifyjs-webpack-plugin": "^1.2.7", "uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "4.19.1", "webpack": "4.19.1",
@ -108,15 +110,30 @@
"watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js", "watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js",
"build": "grunt build", "build": "grunt build",
"test": "grunt test", "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", "jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js", "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": { "lint-staged": {
"*.{ts,tsx}": ["prettier --write", "git add"], "*.{ts,tsx}": [
"*.scss": ["prettier --write", "git add"], "prettier --write",
"*pkg/**/*.go": ["gofmt -w -s", "git add"] "git add"
],
"*.scss": [
"prettier --write",
"git add"
],
"*pkg/**/*.go": [
"gofmt -w -s",
"git add"
]
}, },
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
@ -126,6 +143,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"@torkelo/react-select": "2.1.1",
"angular": "1.6.6", "angular": "1.6.6",
"angular-bindonce": "0.3.1", "angular-bindonce": "0.3.1",
"angular-native-dragdrop": "1.2.2", "angular-native-dragdrop": "1.2.2",
@ -133,7 +151,7 @@
"angular-sanitize": "1.6.6", "angular-sanitize": "1.6.6",
"baron": "^3.0.3", "baron": "^3.0.3",
"brace": "^0.10.0", "brace": "^0.10.0",
"classnames": "^2.2.5", "classnames": "^2.2.6",
"clipboard": "^1.7.1", "clipboard": "^1.7.1",
"d3": "^4.11.0", "d3": "^4.11.0",
"d3-scale-chromatic": "^1.3.0", "d3-scale-chromatic": "^1.3.0",
@ -152,10 +170,9 @@
"react-custom-scrollbars": "^4.2.1", "react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.6.3", "react-dom": "^16.6.3",
"react-grid-layout": "0.16.6", "react-grid-layout": "0.16.6",
"react-popper": "^1.3.0",
"react-highlight-words": "0.11.0", "react-highlight-words": "0.11.0",
"react-popper": "^1.3.0",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"@torkelo/react-select": "2.1.1",
"react-sizeme": "^2.3.6", "react-sizeme": "^2.3.6",
"react-table": "^6.8.6", "react-table": "^6.8.6",
"react-transition-group": "^2.2.1", "react-transition-group": "^2.2.1",
@ -165,18 +182,26 @@
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"remarkable": "^1.7.1", "remarkable": "^1.7.1",
"rst2html": "github:thoward/rst2html#990cb89", "rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^5.4.3", "rxjs": "^6.3.3",
"slate": "^0.33.4", "slate": "^0.33.4",
"slate-plain-serializer": "^0.5.10", "slate-plain-serializer": "^0.5.10",
"slate-prism": "^0.5.0", "slate-prism": "^0.5.0",
"slate-react": "^0.12.4", "slate-react": "^0.12.4",
"tether": "^1.4.0", "tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master", "tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.1"
"tslint-react": "^3.6.0"
}, },
"resolutions": { "resolutions": {
"caniuse-db": "1.0.30000772", "caniuse-db": "1.0.30000772",
"**/@types/react": "16.7.6" "**/@types/react": "16.7.6"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/@types/*",
"**/@types/*/**"
]
} }
} }

View File

@ -0,0 +1,4 @@
# Shared build scripts
Shared build scripts for plugins & internal packages.

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
# Grafana (WIP) shared component library
Used by internal & external plugins.

View File

@ -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"
}
}

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import DeleteButton from './DeleteButton'; import { DeleteButton } from './DeleteButton';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
describe('DeleteButton', () => { describe('DeleteButton', () => {
let wrapper; let wrapper: any;
let deleted; let deleted: any;
beforeAll(() => { beforeAll(() => {
deleted = false; deleted = false;
@ -12,7 +12,8 @@ describe('DeleteButton', () => {
function deleteItem() { function deleteItem() {
deleted = true; deleted = true;
} }
wrapper = shallow(<DeleteButton onConfirmDelete={() => deleteItem()} />);
wrapper = shallow(<DeleteButton onConfirm={() => deleteItem()} />);
}); });
it('should show confirm delete when clicked', () => { it('should show confirm delete when clicked', () => {

View File

@ -1,19 +1,19 @@
import React, { PureComponent } from 'react'; import React, { PureComponent, SyntheticEvent } from 'react';
export interface DeleteButtonProps { interface Props {
onConfirmDelete(); onConfirm(): void;
} }
export interface DeleteButtonStates { interface State {
showConfirm: boolean; showConfirm: boolean;
} }
export default class DeleteButton extends PureComponent<DeleteButtonProps, DeleteButtonStates> { export class DeleteButton extends PureComponent<Props, State> {
state: DeleteButtonStates = { state: State = {
showConfirm: false, showConfirm: false,
}; };
onClickDelete = event => { onClickDelete = (event: SyntheticEvent) => {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
@ -23,7 +23,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
}); });
}; };
onClickCancel = event => { onClickCancel = (event: SyntheticEvent) => {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
@ -33,7 +33,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
}; };
render() { render() {
const onClickConfirm = this.props.onConfirmDelete; const { onConfirm } = this.props;
let showConfirm; let showConfirm;
let showDeleteButton; let showDeleteButton;
@ -55,7 +55,7 @@ export default class DeleteButton extends PureComponent<DeleteButtonProps, Delet
<a className="btn btn-small" onClick={this.onClickCancel}> <a className="btn btn-small" onClick={this.onClickCancel}>
Cancel Cancel
</a> </a>
<a className="btn btn-danger btn-small" onClick={onClickConfirm}> <a className="btn btn-danger btn-small" onClick={onConfirm}>
Confirm Delete Confirm Delete
</a> </a>
</span> </span>

View File

@ -0,0 +1 @@
@import 'DeleteButton/DeleteButton';

View File

@ -0,0 +1 @@
export { DeleteButton } from './DeleteButton/DeleteButton';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { GfFormLabel } from './GfFormLabel/GfFormLabel';

View File

@ -0,0 +1 @@
@import 'components/index';

View File

@ -0,0 +1,5 @@
export * from './components';
export * from './visualizations';
export * from './types';
export * from './utils';
export * from './forms';

View File

@ -0,0 +1,3 @@
export * from './series';
export * from './time';
export * from './panel';

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './processTimeSeries';

View File

@ -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;
}

View File

@ -1,11 +1,9 @@
// Libraries // Libraries
import $ from 'jquery'; import $ from 'jquery';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.time';
// Types // Types
import { TimeRange, TimeSeriesVMs } from 'app/types'; import { TimeRange, TimeSeriesVMs } from '../../types';
interface GraphProps { interface GraphProps {
timeSeries: TimeSeriesVMs; timeSeries: TimeSeriesVMs;
@ -24,7 +22,7 @@ export class Graph extends PureComponent<GraphProps> {
showBars: false, showBars: false,
}; };
element: HTMLElement; element: HTMLElement | null;
componentDidUpdate() { componentDidUpdate() {
this.draw(); this.draw();
@ -35,6 +33,10 @@ export class Graph extends PureComponent<GraphProps> {
} }
draw() { draw() {
if (this.element === null) {
return;
}
const { width, timeSeries, timeRange, showLines, showBars, showPoints } = this.props; const { width, timeSeries, timeRange, showLines, showBars, showPoints } = this.props;
if (!width) { if (!width) {
@ -76,7 +78,7 @@ export class Graph extends PureComponent<GraphProps> {
max: max, max: max,
label: 'Datetime', label: 'Datetime',
ticks: ticks, ticks: ticks,
timeformat: time_format(ticks, min, max), timeformat: timeFormat(ticks, min, max),
}, },
grid: { grid: {
minBorderMargin: 0, minBorderMargin: 0,
@ -109,7 +111,7 @@ export class Graph extends PureComponent<GraphProps> {
} }
// Copied from graph.ts // Copied from graph.ts
function time_format(ticks, min, max) { function timeFormat(ticks: number, min: number, max: number): string {
if (min && max && ticks) { if (min && max && ticks) {
const range = max - min; const range = max - min;
const secPerTick = range / ticks / 1000; const secPerTick = range / ticks / 1000;

View File

@ -0,0 +1 @@
export { Graph } from './Graph/Graph';

View File

@ -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
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -292,6 +292,8 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1), 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) searchResult, err = a.conn.Search(&searchReq)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, models.ErrDashboardFolderCannotHaveParent return nil, models.ErrDashboardFolderCannotHaveParent
} }
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) { if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) {
return nil, models.ErrDashboardFolderNameExists return nil, models.ErrDashboardFolderNameExists
} }
@ -175,7 +175,9 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
dto.User = &models.SignedInUser{ dto.User = &models.SignedInUser{
UserId: 0, UserId: 0,
OrgRole: models.ROLE_ADMIN, OrgRole: models.ROLE_ADMIN,
OrgId: dto.OrgId,
} }
cmd, err := dr.buildSaveDashboardCommand(dto, true, false) cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -69,11 +70,14 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
return err return err
} }
defer resp.Body.Close()
if resp.StatusCode/100 == 2 { if resp.StatusCode/100 == 2 {
// flushing the body enables the transport to reuse the same connection
io.Copy(ioutil.Discard, resp.Body)
return nil return nil
} }
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err

View File

@ -29,18 +29,22 @@ import (
// MysqlStore represents a mysql session store implementation. // MysqlStore represents a mysql session store implementation.
type MysqlStore struct { type MysqlStore struct {
c *sql.DB c *sql.DB
sid string sid string
lock sync.RWMutex lock sync.RWMutex
data map[interface{}]interface{} data map[interface{}]interface{}
expiry int64
dirty bool
} }
// NewMysqlStore creates and returns a mysql session store. // 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{ return &MysqlStore{
c: c, c: c,
sid: sid, sid: sid,
data: kv, data: kv,
expiry: expiry,
dirty: false,
} }
} }
@ -50,6 +54,7 @@ func (s *MysqlStore) Set(key, val interface{}) error {
defer s.lock.Unlock() defer s.lock.Unlock()
s.data[key] = val s.data[key] = val
s.dirty = true
return nil return nil
} }
@ -67,6 +72,7 @@ func (s *MysqlStore) Delete(key interface{}) error {
defer s.lock.Unlock() defer s.lock.Unlock()
delete(s.data, key) delete(s.data, key)
s.dirty = true
return nil return nil
} }
@ -77,13 +83,20 @@ func (s *MysqlStore) ID() string {
// Release releases resource and save data to provider. // Release releases resource and save data to provider.
func (s *MysqlStore) Release() error { func (s *MysqlStore) Release() error {
newExpiry := time.Now().Unix()
if !s.dirty && (s.expiry+60) >= newExpiry {
return nil
}
data, err := session.EncodeGob(s.data) data, err := session.EncodeGob(s.data)
if err != nil { if err != nil {
return err return err
} }
_, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?", _, 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 return err
} }
@ -93,6 +106,7 @@ func (s *MysqlStore) Flush() error {
defer s.lock.Unlock() defer s.lock.Unlock()
s.data = make(map[interface{}]interface{}) s.data = make(map[interface{}]interface{})
s.dirty = true
return nil return nil
} }
@ -117,11 +131,12 @@ func (p *MysqlProvider) Init(expire int64, connStr string) (err error) {
// Read returns raw session store by session ID. // Read returns raw session store by session ID.
func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { func (p *MysqlProvider) Read(sid string) (session.RawStore, error) {
expiry := time.Now().Unix()
var data []byte 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 { if err == sql.ErrNoRows {
_, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)",
sid, "", time.Now().Unix()) sid, "", expiry)
} }
if err != nil { if err != nil {
return nil, err 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. // Exist returns true if session with given ID exists.

View File

@ -2,12 +2,47 @@ package migrator
type MigrationCondition interface { type MigrationCondition interface {
Sql(dialect Dialect) (string, []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 TableName string
IndexName string
} }
func (c *IfTableExistsCondition) Sql(dialect Dialect) (string, []interface{}) { func (c *IfIndexExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
return dialect.TableCheckSql(c.TableName) 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)
} }

View File

@ -29,10 +29,12 @@ type Dialect interface {
DropTable(tableName string) string DropTable(tableName string) string
DropIndexSql(tableName string, index *Index) string DropIndexSql(tableName string, index *Index) string
TableCheckSql(tableName string) (string, []interface{})
RenameTable(oldName string, newName string) string RenameTable(oldName string, newName string) string
UpdateTableSql(tableName string, columns []*Column) string UpdateTableSql(tableName string, columns []*Column) string
IndexCheckSql(tableName, indexName string) (string, []interface{})
ColumnCheckSql(tableName, columnName string) (string, []interface{})
ColString(*Column) string ColString(*Column) string
ColStringNoPk(*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)) 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 { func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
quote := db.dialect.Quote quote := db.dialect.Quote
name := index.XName(tableName) name := index.XName(tableName)

View File

@ -85,7 +85,9 @@ type AddColumnMigration struct {
} }
func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration { 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 { func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
@ -109,7 +111,9 @@ type AddIndexMigration struct {
} }
func NewAddIndexMigration(table Table, index *Index) *AddIndexMigration { 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 { func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration {
@ -128,7 +132,9 @@ type DropIndexMigration struct {
} }
func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration { 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 { func (m *DropIndexMigration) Sql(dialect Dialect) string {
@ -179,11 +185,6 @@ func NewRenameTableMigration(oldName string, newName string) *RenameTableMigrati
return &RenameTableMigration{oldName: oldName, newName: newName} 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 { func (m *RenameTableMigration) Rename(oldName string, newName string) *RenameTableMigration {
m.oldName = oldName m.oldName = oldName
m.newName = newName m.newName = newName
@ -212,11 +213,6 @@ func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap ma
return m return m
} }
func (m *CopyTableDataMigration) IfTableExists(tableName string) *CopyTableDataMigration {
m.Condition = &IfTableExistsCondition{TableName: tableName}
return m
}
func (m *CopyTableDataMigration) Sql(d Dialect) string { func (m *CopyTableDataMigration) Sql(d Dialect) string {
return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols) return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols)
} }

View File

@ -94,8 +94,6 @@ func (mg *Migrator) Start() error {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
mg.Logger.Debug("Executing", "sql", sql)
err := mg.inTransaction(func(sess *xorm.Session) error { err := mg.inTransaction(func(sess *xorm.Session) error {
err := mg.exec(m, sess) err := mg.exec(m, sess)
if err != nil { if err != nil {
@ -123,18 +121,30 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
condition := m.GetCondition() condition := m.GetCondition()
if condition != nil { if condition != nil {
sql, args := condition.Sql(mg.Dialect) sql, args := condition.Sql(mg.Dialect)
results, err := sess.SQL(sql).Query(args...)
if err != nil || len(results) == 0 { if sql != "" {
mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id()) mg.Logger.Debug("Executing migration condition sql", "id", m.Id(), "sql", sql, "args", args)
return sess.Rollback() 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 var err error
if codeMigration, ok := m.(CodeMigration); ok { if codeMigration, ok := m.(CodeMigration); ok {
mg.Logger.Debug("Executing code migration", "id", m.Id())
err = codeMigration.Exec(sess, mg) err = codeMigration.Exec(sess, mg)
} else { } 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 { if err != nil {

View File

@ -90,12 +90,6 @@ func (db *Mysql) SqlType(c *Column) string {
return res 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 { func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
var statements = []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, ", ") + ";" 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 { func (db *Mysql) CleanDB() error {
tables, _ := db.engine.DBMetas() tables, _ := db.engine.DBMetas()
sess := db.engine.NewSession() sess := db.engine.NewSession()

View File

@ -101,9 +101,9 @@ func (db *Postgres) SqlType(c *Column) string {
return res return res
} }
func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) { func (db *Postgres) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
args := []interface{}{"grafana", tableName} args := []interface{}{tableName, indexName}
sql := "SELECT table_name FROM information_schema.tables WHERE table_schema=? and table_name=?" sql := "SELECT 1 FROM " + db.Quote("pg_indexes") + " WHERE" + db.Quote("tablename") + "=? AND " + db.Quote("indexname") + "=?"
return sql, args return sql, args
} }

View File

@ -68,9 +68,10 @@ func (db *Sqlite3) SqlType(c *Column) string {
} }
} }
func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) { func (db *Sqlite3) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
args := []interface{}{tableName} args := []interface{}{tableName, indexName}
return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args 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 { func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string {

View File

@ -243,7 +243,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
ss.dbCfg.Path = filepath.Join(ss.Cfg.DataPath, ss.dbCfg.Path) ss.dbCfg.Path = filepath.Join(ss.Cfg.DataPath, ss.dbCfg.Path)
} }
os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm) 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: default:
return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type) 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.ClientCertPath = sec.Key("client_cert_path").String()
ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String() ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String()
ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db") 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 { func InitTestDB(t *testing.T) *SqlStore {
@ -391,13 +393,20 @@ func IsTestDbPostgres() bool {
} }
type DatabaseConfig struct { type DatabaseConfig struct {
Type, Host, Name, User, Pwd, Path, SslMode string Type string
CaCertPath string Host string
ClientKeyPath string Name string
ClientCertPath string User string
ServerCertName string Pwd string
ConnectionString string Path string
MaxOpenConn int SslMode string
MaxIdleConn int CaCertPath string
ConnMaxLifetime int ClientKeyPath string
ClientCertPath string
ServerCertName string
ConnectionString string
MaxOpenConn int
MaxIdleConn int
ConnMaxLifetime int
CacheMode string
} }

View File

@ -16,6 +16,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data
rawQuery := model.Get("query").MustString("") rawQuery := model.Get("query").MustString("")
useRawQuery := model.Get("rawQuery").MustBool(false) useRawQuery := model.Get("rawQuery").MustBool(false)
alias := model.Get("alias").MustString("") alias := model.Get("alias").MustString("")
tz := model.Get("tz").MustString("")
measurement := model.Get("measurement").MustString("") measurement := model.Get("measurement").MustString("")
@ -55,6 +56,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data
Interval: parsedInterval, Interval: parsedInterval,
Alias: alias, Alias: alias,
UseRawQuery: useRawQuery, UseRawQuery: useRawQuery,
Tz: tz,
}, nil }, nil
} }

View File

@ -41,6 +41,7 @@ func TestInfluxdbQueryParser(t *testing.T) {
} }
], ],
"measurement": "logins.count", "measurement": "logins.count",
"tz": "Europe/Paris",
"policy": "default", "policy": "default",
"refId": "B", "refId": "B",
"resultFormat": "time_series", "resultFormat": "time_series",
@ -115,6 +116,7 @@ func TestInfluxdbQueryParser(t *testing.T) {
So(len(res.GroupBy), ShouldEqual, 3) So(len(res.GroupBy), ShouldEqual, 3)
So(len(res.Selects), ShouldEqual, 3) So(len(res.Selects), ShouldEqual, 3)
So(len(res.Tags), ShouldEqual, 2) So(len(res.Tags), ShouldEqual, 2)
So(res.Tz, ShouldEqual, "Europe/Paris")
So(res.Interval, ShouldEqual, time.Second*20) So(res.Interval, ShouldEqual, time.Second*20)
So(res.Alias, ShouldEqual, "serie alias") So(res.Alias, ShouldEqual, "serie alias")
}) })

View File

@ -13,6 +13,7 @@ type Query struct {
UseRawQuery bool UseRawQuery bool
Alias string Alias string
Interval time.Duration Interval time.Duration
Tz string
} }
type Tag struct { type Tag struct {

View File

@ -26,6 +26,7 @@ func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) {
res += query.renderWhereClause() res += query.renderWhereClause()
res += query.renderTimeFilter(queryContext) res += query.renderTimeFilter(queryContext)
res += query.renderGroupBy(queryContext) res += query.renderGroupBy(queryContext)
res += query.renderTz()
} }
calculator := tsdb.NewIntervalCalculator(&tsdb.IntervalOptions{}) calculator := tsdb.NewIntervalCalculator(&tsdb.IntervalOptions{})
@ -154,3 +155,12 @@ func (query *Query) renderGroupBy(queryContext *tsdb.TsdbQuery) string {
return groupBy return groupBy
} }
func (query *Query) renderTz() string {
tz := query.Tz
if tz == "" {
return ""
} else {
return fmt.Sprintf(" tz('%s')", tz)
}
}

View File

@ -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)`) 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() { Convey("can build query with group bys", func() {
query := &Query{ query := &Query{
Selects: []*Select{{*qp1, *qp2}}, Selects: []*Select{{*qp1, *qp2}},

View File

@ -86,11 +86,11 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
return "", fmt.Errorf("missing time column argument for macro %v", name) return "", fmt.Errorf("missing time column argument for macro %v", name)
} }
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
case "__timeFrom": case "__timeFrom":
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil
case "__timeTo": case "__timeTo":
return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil
case "__timeGroup": case "__timeGroup":
if len(args) < 2 { if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

View File

@ -41,7 +41,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
}) })
Convey("interpolate __timeFrom function", func() { Convey("interpolate __timeFrom function", func() {
@ -138,7 +138,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
}) })
Convey("interpolate __unixEpochFilter function", func() { Convey("interpolate __unixEpochFilter function", func() {
@ -158,7 +158,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
}) })
Convey("interpolate __unixEpochFilter function", func() { Convey("interpolate __unixEpochFilter function", func() {
@ -168,5 +168,22 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix())) So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
}) })
}) })
Convey("Given a time range between 1960-02-01 07:00:00.5 and 1980-02-03 08:00:00.5", func() {
from := time.Date(1960, 2, 1, 7, 0, 0, 500e6, time.UTC)
to := time.Date(1980, 2, 3, 8, 0, 0, 500e6, time.UTC)
timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
So(from.Format(time.RFC3339Nano), ShouldEqual, "1960-02-01T07:00:00.5Z")
So(to.Format(time.RFC3339Nano), ShouldEqual, "1980-02-03T08:00:00.5Z")
Convey("interpolate __timeFilter function", func() {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano)))
})
})
}) })
} }

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -1,3 +1 @@
export { Element } from './Element';
export { Input } from './Input'; export { Input } from './Input';
export { Label } from './Label';

View File

@ -52,7 +52,11 @@ export const ToggleButton: SFC<ToggleButtonProps> = ({
); );
if (tooltip) { if (tooltip) {
return <Tooltip content={tooltip}>{button}</Tooltip>; return (
<Tooltip content={tooltip} placement="bottom">
{button}
</Tooltip>
);
} else { } else {
return button; return button;
} }

View File

@ -69,7 +69,7 @@ function bootstrapTagsinput() {
}, },
}); });
select.on('itemAdded', event => { select.on('itemAdded', (event: any) => {
if (scope.model.indexOf(event.item) === -1) { if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item); scope.model.push(event.item);
if (scope.onTagsUpdated) { if (scope.onTagsUpdated) {
@ -85,7 +85,7 @@ function bootstrapTagsinput() {
setColor(event.item, tagElement); setColor(event.item, tagElement);
}); });
select.on('itemRemoved', event => { select.on('itemRemoved', (event: any) => {
const idx = scope.model.indexOf(event.item); const idx = scope.model.indexOf(event.item);
if (idx !== -1) { if (idx !== -1) {
scope.model.splice(idx, 1); scope.model.splice(idx, 1);

View File

@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import config from 'app/core/config'; import config from 'app/core/config';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
export class LiveSrv { export class LiveSrv {
conn: any; conn: any;

View File

@ -2,14 +2,23 @@ import _ from 'lodash';
import { TimeSeries } from 'app/core/core'; import { TimeSeries } from 'app/core/core';
import colors, { getThemeColor } from 'app/core/utils/colors'; 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 { export enum LogLevel {
emerg = 'critical',
alert = 'critical',
crit = 'critical', crit = 'critical',
critical = 'critical', critical = 'critical',
warn = 'warning', warn = 'warning',
warning = 'warning', warning = 'warning',
err = 'error', err = 'error',
eror = 'error',
error = 'error', error = 'error',
info = 'info', info = 'info',
notice = 'info',
dbug = 'debug',
debug = 'debug', debug = 'debug',
trace = 'trace', trace = 'trace',
unkown = 'unkown', unkown = 'unkown',
@ -81,7 +90,9 @@ export interface LogsStream {
export interface LogsStreamEntry { export interface LogsStreamEntry {
line: string; line: string;
timestamp: string; ts: string;
// Legacy, was renamed to ts
timestamp?: string;
} }
export interface LogsStreamLabels { export interface LogsStreamLabels {

View File

@ -14,7 +14,6 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
datasourceError: null, datasourceError: null,
datasourceLoading: null, datasourceLoading: null,
datasourceMissing: false, datasourceMissing: false,
datasourceName: '',
exploreDatasources: [], exploreDatasources: [],
graphInterval: 1000, graphInterval: 1000,
history: [], history: [],
@ -69,7 +68,7 @@ describe('state functions', () => {
it('returns url parameter value for a state object', () => { it('returns url parameter value for a state object', () => {
const state = { const state = {
...DEFAULT_EXPLORE_STATE, ...DEFAULT_EXPLORE_STATE,
datasourceName: 'foo', initialDatasource: 'foo',
range: { range: {
from: 'now-5h', from: 'now-5h',
to: 'now', to: 'now',
@ -94,7 +93,7 @@ describe('state functions', () => {
it('returns url parameter value for a state object', () => { it('returns url parameter value for a state object', () => {
const state = { const state = {
...DEFAULT_EXPLORE_STATE, ...DEFAULT_EXPLORE_STATE,
datasourceName: 'foo', initialDatasource: 'foo',
range: { range: {
from: 'now-5h', from: 'now-5h',
to: 'now', to: 'now',
@ -120,7 +119,7 @@ describe('state functions', () => {
it('can parse the serialized state into the original state', () => { it('can parse the serialized state into the original state', () => {
const state = { const state = {
...DEFAULT_EXPLORE_STATE, ...DEFAULT_EXPLORE_STATE,
datasourceName: 'foo', initialDatasource: 'foo',
range: { range: {
from: 'now - 5h', from: 'now - 5h',
to: 'now', to: 'now',
@ -144,7 +143,7 @@ describe('state functions', () => {
const resultState = { const resultState = {
...rest, ...rest,
datasource: DEFAULT_EXPLORE_STATE.datasource, datasource: DEFAULT_EXPLORE_STATE.datasource,
datasourceName: datasource, initialDatasource: datasource,
initialQueries: queries, initialQueries: queries,
}; };

View File

@ -9,7 +9,8 @@ import { parse as parseDate } from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore'; 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 = { export const DEFAULT_RANGE = {
from: 'now-6h', from: 'now-6h',
@ -104,7 +105,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
const urlState: ExploreUrlState = { const urlState: ExploreUrlState = {
datasource: state.datasourceName, datasource: state.initialDatasource,
queries: state.initialQueries.map(clearQueryKeys), queries: state.initialQueries.map(clearQueryKeys),
range: state.range, range: state.range,
}; };

View File

@ -629,6 +629,8 @@ kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³');
kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³'); kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³');
kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³'); kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³');
kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³'); 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 // Time
kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
@ -1209,6 +1211,8 @@ kbn.getUnitFormats = () => {
{ text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' }, { text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' },
{ text: 'gram per cubic meter (g/m³)', value: 'congm3' }, { text: 'gram per cubic meter (g/m³)', value: 'congm3' },
{ text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' }, { 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' },
], ],
}, },
]; ];

View File

@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { RawTimeRange } from 'app/types/series'; import { RawTimeRange } from '@grafana/ui';
import * as dateMath from './datemath'; import * as dateMath from './datemath';

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import classNames from 'classnames/bind'; import classNames from 'classnames';
import { AlertRule } from '../../types'; import { AlertRule } from '../../types';
export interface Props { export interface Props {
@ -23,7 +23,7 @@ class AlertRuleItem extends PureComponent<Props> {
render() { render() {
const { rule, onTogglePause } = this.props; const { rule, onTogglePause } = this.props;
const stateClass = classNames({ const iconClassName = classNames({
fa: true, fa: true,
'fa-play': rule.state === 'paused', 'fa-play': rule.state === 'paused',
'fa-pause': 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" title="Pausing an alert rule prevents it from executing"
onClick={onTogglePause} onClick={onTogglePause}
> >
<i className={stateClass} /> <i className={iconClassName} />
</button> </button>
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule"> <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" /> <i className="icon-gf icon-gf-settings" />

View File

@ -13,7 +13,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config'; import config from 'app/core/config';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { DeleteButton } from '@grafana/ui';
export interface Props { export interface Props {
navModel: NavModel; navModel: NavModel;
@ -224,7 +224,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
<td>{key.name}</td> <td>{key.name}</td>
<td>{key.role}</td> <td>{key.role}</td>
<td> <td>
<DeleteButton onConfirmDelete={() => this.onDeleteApiKey(key)} /> <DeleteButton onConfirm={() => this.onDeleteApiKey(key)} />
</td> </td>
</tr> </tr>
); );

View File

@ -160,14 +160,14 @@ export class DashboardPanel extends PureComponent<Props, State> {
return ( return (
<div className={containerClass}> <div className={containerClass}>
<PanelResizer <PanelResizer
isEditing={!!isEditing} isEditing={isEditing}
panel={panel} panel={panel}
render={(panelHeight: number | 'inherit') => ( render={styles => (
<div <div
className={panelWrapperClass} className={panelWrapperClass}
onMouseEnter={this.onMouseEnter} onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
style={{ height: panelHeight }} style={styles}
> >
{plugin.exports.Panel && this.renderReactPanel()} {plugin.exports.Panel && this.renderReactPanel()}
{plugin.exports.PanelCtrl && this.renderAngularPanel()} {plugin.exports.PanelCtrl && this.renderAngularPanel()}

View File

@ -8,7 +8,8 @@ import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
// Types // 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 { interface RenderProps {
loading: LoadingState; loading: LoadingState;

View File

@ -16,7 +16,8 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
// Types // Types
import { PanelModel } from '../panel_model'; import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_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 { export interface Props {
panel: PanelModel; panel: PanelModel;

View File

@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelModel } from 'app/features/dashboard/panel_model';
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu'; import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
import { PanelMenuItem } from 'app/types/panel'; import { PanelMenuItem } from '@grafana/ui';
export interface Props { export interface Props {
panel: PanelModel; panel: PanelModel;

View File

@ -1,5 +1,5 @@
import React, { SFC } from 'react'; import React, { SFC } from 'react';
import { PanelMenuItem } from 'app/types/panel'; import { PanelMenuItem } from '@grafana/ui';
interface Props { interface Props {
children: any; children: any;

View File

@ -1,6 +1,10 @@
// Libraries
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { PanelPlugin, PanelProps } from 'app/types';
// Types
import { PanelProps } from '@grafana/ui';
import { PanelPlugin } from 'app/types';
interface Props { interface Props {
pluginId: string; pluginId: string;

View File

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
@ -6,7 +6,7 @@ import { PanelModel } from '../panel_model';
interface Props { interface Props {
isEditing: boolean; isEditing: boolean;
render: (height: number | 'inherit') => JSX.Element; render: (styles: object) => JSX.Element;
panel: PanelModel; panel: PanelModel;
} }
@ -19,6 +19,7 @@ export class PanelResizer extends PureComponent<Props, State> {
prevEditorHeight: number; prevEditorHeight: number;
throttledChangeHeight: (height: number) => void; throttledChangeHeight: (height: number) => void;
throttledResizeDone: () => void; throttledResizeDone: () => void;
noStyles: object = {};
constructor(props) { constructor(props) {
super(props); super(props);
@ -65,7 +66,7 @@ export class PanelResizer extends PureComponent<Props, State> {
return ( return (
<> <>
{render(isEditing ? editorHeight : 'inherit')} {render(isEditing ? {height: editorHeight} : this.noStyles)}
{isEditing && ( {isEditing && (
<div className="panel-editor-container__resizer"> <div className="panel-editor-container__resizer">
<Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}> <Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>

View File

@ -10,6 +10,7 @@ import { Input } from 'app/core/components/Form';
import { EventsWithValidation } from 'app/core/components/Form/Input'; import { EventsWithValidation } from 'app/core/components/Form/Input';
import { InputStatus } from 'app/core/components/Form/Input'; import { InputStatus } from 'app/core/components/Form/Input';
import DataSourceOption from './DataSourceOption'; import DataSourceOption from './DataSourceOption';
import { GfFormLabel } from '@grafana/ui';
// Types // Types
import { PanelModel } from '../panel_model'; import { PanelModel } from '../panel_model';
@ -163,7 +164,7 @@ export class QueryOptions extends PureComponent<Props, State> {
{this.renderOptions()} {this.renderOptions()}
<div className="gf-form"> <div className="gf-form">
<span className="gf-form-label">Relative time</span> <GfFormLabel>Relative time</GfFormLabel>
<Input <Input
type="text" type="text"
className="width-6" className="width-6"

View File

@ -6,6 +6,7 @@ function dashLinksContainer() {
return { return {
scope: { scope: {
links: '=', links: '=',
dashboard: '=',
}, },
restrict: 'E', restrict: 'E',
controller: 'DashLinksContainerCtrl', controller: 'DashLinksContainerCtrl',
@ -20,6 +21,8 @@ function dashLink($compile, $sanitize, linkSrv) {
restrict: 'E', restrict: 'E',
link: (scope, elem) => { link: (scope, elem) => {
const link = scope.link; const link = scope.link;
const dashboard = scope.dashboard;
let template = let template =
'<div class="gf-form">' + '<div class="gf-form">' +
'<a class="pointer gf-form-label" data-placement="bottom"' + '<a class="pointer gf-form-label" data-placement="bottom"' +
@ -76,7 +79,7 @@ function dashLink($compile, $sanitize, linkSrv) {
} }
update(); update();
scope.$on('refresh', update); dashboard.events.on('refresh', update, scope);
}, },
}; };
} }

View File

@ -20,7 +20,7 @@
</div> </div>
<div ng-if="ctrl.dashboard.links.length > 0" > <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>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -6,9 +6,9 @@ import _ from 'lodash';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import * as dateMath from 'app/core/utils/datemath'; import * as dateMath from 'app/core/utils/datemath';
// Types
import { TimeRange } from 'app/types'; // Types
import { TimeRange } from '@grafana/ui';
export class TimeSrv { export class TimeSrv {
time: any; time: any;

View File

@ -4,7 +4,7 @@ import { store } from 'app/store/store';
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/dashboard_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) => { export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
const onViewPanel = () => { const onViewPanel = () => {

View File

@ -4,7 +4,7 @@ import store from 'app/core/store';
// Models // Models
import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelModel } from 'app/features/dashboard/panel_model';
import { TimeRange } from 'app/types/series'; import { TimeRange } from '@grafana/ui';
// Utils // Utils
import { isString as _isString } from 'lodash'; import { isString as _isString } from 'lodash';

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import classNames from 'classnames/bind'; import classNames from 'classnames';
import DataSourcesListItem from './DataSourcesListItem'; import DataSourcesListItem from './DataSourcesListItem';
import { DataSource } from 'app/types'; import { DataSource } from 'app/types';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';

View File

@ -11,7 +11,8 @@ import {
QueryHintGetter, QueryHintGetter,
QueryHint, QueryHint,
} from 'app/types/explore'; } 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 store from 'app/core/store';
import { import {
DEFAULT_RANGE, DEFAULT_RANGE,
@ -39,6 +40,8 @@ import ErrorBoundary from './ErrorBoundary';
import { Alert } from './Error'; import { Alert } from './Error';
import TimePicker, { parseTime } from './TimePicker'; import TimePicker, { parseTime } from './TimePicker';
const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
interface ExploreProps { interface ExploreProps {
datasourceSrv: DatasourceSrv; datasourceSrv: DatasourceSrv;
onChangeSplit: (split: boolean, state?: ExploreState) => void; onChangeSplit: (split: boolean, state?: ExploreState) => void;
@ -89,6 +92,10 @@ interface ExploreProps {
export class Explore extends React.PureComponent<ExploreProps, ExploreState> { export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
el: any; el: any;
exploreEvents: Emitter; exploreEvents: Emitter;
/**
* Set via URL or local storage
*/
initialDatasource: string;
/** /**
* Current query expressions of the rows including their modifications, used for running queries. * Current query expressions of the rows including their modifications, used for running queries.
* Not kept in component state to prevent edit-render roundtrips. * 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; initialQueries = splitState.initialQueries;
} else { } else {
const { datasource, queries, range } = props.urlState as ExploreUrlState; const { datasource, queries, range } = props.urlState as ExploreUrlState;
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
initialQueries = ensureQueries(queries); initialQueries = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE }; const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
// Millies step for helper bar charts // Millies step for helper bar charts
@ -123,10 +131,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceError: null, datasourceError: null,
datasourceLoading: null, datasourceLoading: null,
datasourceMissing: false, datasourceMissing: false,
datasourceName: datasource,
exploreDatasources: [], exploreDatasources: [],
graphInterval: initialGraphInterval, graphInterval: initialGraphInterval,
graphResult: [], graphResult: [],
initialDatasource,
initialQueries, initialQueries,
history: [], history: [],
logsResult: null, logsResult: null,
@ -150,7 +158,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
async componentDidMount() { async componentDidMount() {
const { datasourceSrv } = this.props; const { datasourceSrv } = this.props;
const { datasourceName } = this.state; const { initialDatasource } = this.state;
if (!datasourceSrv) { if (!datasourceSrv) {
throw new Error('No datasource service passed as props.'); 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) { if (datasources.length > 0) {
this.setState({ datasourceLoading: true, exploreDatasources }); this.setState({ datasourceLoading: true, exploreDatasources });
// Priority: datasource in url, default datasource, first explore datasource // Priority for datasource preselection: URL, localstorage, default datasource
let datasource; let datasource;
if (datasourceName) { if (initialDatasource) {
datasource = await datasourceSrv.get(datasourceName); datasource = await datasourceSrv.get(initialDatasource);
} else { } else {
datasource = await datasourceSrv.get(); datasource = await datasourceSrv.get();
} }
@ -252,13 +260,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
supportsLogs, supportsLogs,
supportsTable, supportsTable,
datasourceLoading: false, datasourceLoading: false,
datasourceName: datasource.name, initialDatasource: datasource.name,
initialQueries: nextQueries, initialQueries: nextQueries,
logsHighlighterExpressions: undefined, logsHighlighterExpressions: undefined,
showingStartPage: Boolean(StartPage), showingStartPage: Boolean(StartPage),
}, },
() => { () => {
if (datasourceError === null) { if (datasourceError === null) {
// Save last-used datasource
store.set(LAST_USED_DATASOURCE_KEY, datasource.name);
this.onSubmit(); this.onSubmit();
} }
} }

View File

@ -8,7 +8,7 @@ import 'vendor/flot/jquery.flot.time';
import 'vendor/flot/jquery.flot.selection'; import 'vendor/flot/jquery.flot.selection';
import 'vendor/flot/jquery.flot.stack'; 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 * as dateMath from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';

View File

@ -4,7 +4,7 @@ import Highlighter from 'react-highlight-words';
import classnames from 'classnames'; import classnames from 'classnames';
import * as rangeUtil from 'app/core/utils/rangeutil'; import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange } from 'app/types/series'; import { RawTimeRange } from '@grafana/ui';
import { import {
LogsDedupDescription, LogsDedupDescription,
LogsDedupStrategy, LogsDedupStrategy,

View File

@ -3,7 +3,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
import { getIntervals } from 'app/core/utils/explore'; import { getIntervals } from 'app/core/utils/explore';
import { DataQuery } from 'app/types'; 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 { getTimeSrv } from 'app/features/dashboard/time_srv';
import 'app/features/plugins/plugin_loader'; import 'app/features/plugins/plugin_loader';

View File

@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
import { Change, Value } from 'slate'; import { Change, Value } from 'slate';
import { Editor } from 'slate-react'; import { Editor } from 'slate-react';
import Plain from 'slate-plain-serializer'; import Plain from 'slate-plain-serializer';
import classnames from 'classnames';
import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore'; import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
@ -30,6 +31,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
export interface QueryFieldProps { export interface QueryFieldProps {
additionalPlugins?: any[]; additionalPlugins?: any[];
cleanText?: (text: string) => string; cleanText?: (text: string) => string;
disabled?: boolean;
initialQuery: string | null; initialQuery: string | null;
onBlur?: () => void; onBlur?: () => void;
onFocus?: () => void; onFocus?: () => void;
@ -78,7 +80,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || ''); this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || '');
// Base plugins // Base plugins
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p); this.plugins = [ClearPlugin(), NewlinePlugin(), ...(props.additionalPlugins || [])].filter(p => p);
this.state = { this.state = {
suggestions: [], suggestions: [],
@ -440,12 +442,17 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
}; };
render() { render() {
const { disabled } = this.props;
const wrapperClassName = classnames('slate-query-field__wrapper', {
'slate-query-field__wrapper--disabled': disabled,
});
return ( return (
<div className="slate-query-field-wrapper"> <div className={wrapperClassName}>
<div className="slate-query-field"> <div className="slate-query-field">
{this.renderMenu()} {this.renderMenu()}
<Editor <Editor
autoCorrect={false} autoCorrect={false}
readOnly={this.props.disabled}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onChange={this.onChange} onChange={this.onChange}

View File

@ -7,7 +7,7 @@ import { Emitter } from 'app/core/utils/emitter';
import QueryEditor from './QueryEditor'; import QueryEditor from './QueryEditor';
import QueryTransactionStatus from './QueryTransactionStatus'; import QueryTransactionStatus from './QueryTransactionStatus';
import { DataSource, DataQuery } from 'app/types'; import { DataSource, DataQuery } from 'app/types';
import { RawTimeRange } from 'app/types/series'; import { RawTimeRange } from '@grafana/ui';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);

View File

@ -3,7 +3,7 @@ import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath'; import * as dateMath from 'app/core/utils/datemath';
import * as rangeUtil from 'app/core/utils/rangeutil'; 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'; const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {

View File

@ -198,11 +198,10 @@ export class PanelCtrl {
} }
calculatePanelHeight() { calculatePanelHeight() {
if (this.panel.fullscreen) { if (this.panel.isEditing) {
const docHeight = $('.react-grid-layout').height(); this.containerHeight = $('.panel-wrapper--edit').height();
const editHeight = Math.floor(docHeight * 0.35); } else if (this.panel.fullscreen) {
const fullscreenHeight = Math.floor(docHeight * 0.8); this.containerHeight = $('.panel-wrapper--view').height();
this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight;
} else { } else {
this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN; this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
} }

View File

@ -22,7 +22,7 @@
<div class="panel-option-section__body"> <div class="panel-option-section__body">
<div class="section"> <div class="section">
<div class="gf-form"> <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> <dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
</div> </div>
<div class="gf-form" ng-show="ctrl.panel.repeat"> <div class="gf-form" ng-show="ctrl.panel.repeat">
@ -42,7 +42,7 @@
</div> </div>
<div class="panel-option-section"> <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"> <div class="panel-option-section__body">
<panel-links-editor panel="ctrl.panel"></panel-links-editor> <panel-links-editor panel="ctrl.panel"></panel-links-editor>
</div> </div>

View File

@ -1,5 +1,5 @@
import React, { SFC } from 'react'; import React, { SFC } from 'react';
import classNames from 'classnames/bind'; import classNames from 'classnames';
import PluginListItem from './PluginListItem'; import PluginListItem from './PluginListItem';
import { Plugin } from 'app/types'; import { Plugin } from 'app/types';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';

View File

@ -26,16 +26,10 @@ import * as ticks from 'app/core/utils/ticks';
import impressionSrv from 'app/core/services/impression_srv'; import impressionSrv from 'app/core/services/impression_srv';
import builtInPlugins from './built_in_plugins'; import builtInPlugins from './built_in_plugins';
import * as d3 from 'd3'; import * as d3 from 'd3';
import * as grafanaUI from '@grafana/ui';
// rxjs // rxjs
import { Observable } from 'rxjs/Observable'; import { Observable, Subject } from 'rxjs';
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';
// add cache busting // add cache busting
const bust = `?_cache=${Date.now()}`; const bust = `?_cache=${Date.now()}`;
@ -71,6 +65,7 @@ function exposeToPlugin(name: string, component: any) {
}); });
} }
exposeToPlugin('@grafana/ui', grafanaUI);
exposeToPlugin('lodash', _); exposeToPlugin('lodash', _);
exposeToPlugin('moment', moment); exposeToPlugin('moment', moment);
exposeToPlugin('jquery', jquery); exposeToPlugin('jquery', jquery);

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import PageHeader from 'app/core/components/PageHeader/PageHeader'; 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 EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import PageLoader from 'app/core/components/PageLoader/PageLoader'; import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { NavModel, Team } from '../../types'; import { NavModel, Team } from '../../types';
@ -58,7 +58,7 @@ export class TeamList extends PureComponent<Props, any> {
<a href={teamUrl}>{team.memberCount}</a> <a href={teamUrl}>{team.memberCount}</a>
</td> </td>
<td className="text-right"> <td className="text-right">
<DeleteButton onConfirmDelete={() => this.deleteTeam(team)} /> <DeleteButton onConfirm={() => this.deleteTeam(team)} />
</td> </td>
</tr> </tr>
); );

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown'; import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker'; 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 { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User } from 'app/types'; import { TeamMember, User } from 'app/types';
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
@ -76,7 +76,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<td>{member.email}</td> <td>{member.email}</td>
{syncEnabled && this.renderLabels(member.labels)} {syncEnabled && this.renderLabels(member.labels)}
<td className="text-right"> <td className="text-right">
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} /> <DeleteButton onConfirm={() => this.onRemoveMember(member)} />
</td> </td>
</tr> </tr>
); );

View File

@ -124,7 +124,7 @@ exports[`Render should render teams table 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -174,7 +174,7 @@ exports[`Render should render teams table 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -224,7 +224,7 @@ exports[`Render should render teams table 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -274,7 +274,7 @@ exports[`Render should render teams table 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -324,7 +324,7 @@ exports[`Render should render teams table 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>

View File

@ -204,7 +204,7 @@ exports[`Render should render team members 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -229,7 +229,7 @@ exports[`Render should render team members 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -254,7 +254,7 @@ exports[`Render should render team members 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -279,7 +279,7 @@ exports[`Render should render team members 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -304,7 +304,7 @@ exports[`Render should render team members 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -441,7 +441,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -482,7 +482,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -523,7 +523,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -564,7 +564,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>
@ -605,7 +605,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="text-right" className="text-right"
> >
<DeleteButton <DeleteButton
onConfirmDelete={[Function]} onConfirm={[Function]}
/> />
</td> </td>
</tr> </tr>

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classNames from 'classnames/bind'; import classNames from 'classnames';
import { setUsersSearchQuery } from './state/actions'; import { setUsersSearchQuery } from './state/actions';
import { getInviteesCount, getUsersSearchQuery } from './state/selectors'; import { getInviteesCount, getUsersSearchQuery } from './state/selectors';

View File

@ -256,6 +256,10 @@ export default class InfluxQuery {
query += ' SLIMIT ' + target.slimit; query += ' SLIMIT ' + target.slimit;
} }
if (target.tz) {
query += " tz('" + target.tz + "')";
}
return query; return query;
} }

View File

@ -119,6 +119,16 @@
</div> </div>
</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-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">FORMAT AS</label> <label class="gf-form-label query-keyword width-7">FORMAT AS</label>

View File

@ -100,6 +100,9 @@ export class InfluxQueryCtrl extends QueryCtrl {
if (!this.target.slimit) { if (!this.target.slimit) {
options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' })); options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' }));
} }
if (!this.target.tz) {
options.push(this.uiSegmentSrv.newSegment({ value: 'tz' }));
}
if (this.target.orderByTime === 'ASC') { if (this.target.orderByTime === 'ASC') {
options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' })); options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' }));
} }
@ -124,6 +127,10 @@ export class InfluxQueryCtrl extends QueryCtrl {
this.target.slimit = 10; this.target.slimit = 10;
break; break;
} }
case 'tz': {
this.target.tz = 'UTC';
break;
}
case 'ORDER BY time DESC': { case 'ORDER BY time DESC': {
this.target.orderByTime = 'DESC'; this.target.orderByTime = 'DESC';
break; break;

View File

@ -2,14 +2,23 @@ import React from 'react';
const CHEAT_SHEET_ITEMS = [ 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"}', expression: '{job="default/prometheus"}',
label: 'Returns all log lines emitted by instances of this job.', label: 'Returns all log lines emitted by instances of this job.',
}, },
{ {
title: 'Search For Text', title: 'Combine stream selectors',
expression: '{app="cassandra"} Maximum memory usage', expression: '{app="cassandra",namespace="prod"}',
label: 'Returns all log lines for the selector and highlights the given text in the results.', 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 => ( {CHEAT_SHEET_ITEMS.map(item => (
<div className="cheat-sheet-item" key={item.expression}> <div className="cheat-sheet-item" key={item.expression}>
<div className="cheat-sheet-item__title">{item.title}</div> <div className="cheat-sheet-item__title">{item.title}</div>
<div {item.expression && (
className="cheat-sheet-item__expression" <div
onClick={e => props.onClickExample({ refId: '1', expr: item.expression })} className="cheat-sheet-item__expression"
> onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
<code>{item.expression}</code> >
</div> <code>{item.expression}</code>
</div>
)}
<div className="cheat-sheet-item__label">{item.label}</div> <div className="cheat-sheet-item__label">{item.label}</div>
</div> </div>
))} ))}

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