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)
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
### Bug fixes
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)

View File

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

View File

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

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
# Limit where auth proxy requests come from by configuring a list of IP addresses.
# This can be used to prevent users spoofing the X-WEBAUTH-USER header.
# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
whitelist =
# Optionally define more headers to sync other user attributes
# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL``
# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`
headers =
```

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.
### cache_mode
For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared)
Defaults to private.
<hr />
## [security]

View File

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

View File

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

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

View File

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

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

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),
}
a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
searchResult, err = a.conn.Search(&searchReq)
if err != nil {
return nil, err

View File

@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
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
}
@ -175,7 +175,9 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
OrgId: dto.OrgId,
}
cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
if err != nil {
return nil, err

View File

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

View File

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

View File

@ -2,12 +2,47 @@ package migrator
type MigrationCondition interface {
Sql(dialect Dialect) (string, []interface{})
IsFulfilled(results []map[string][]byte) bool
}
type IfTableExistsCondition struct {
type ExistsMigrationCondition struct{}
func (c *ExistsMigrationCondition) IsFulfilled(results []map[string][]byte) bool {
return len(results) >= 1
}
type NotExistsMigrationCondition struct{}
func (c *NotExistsMigrationCondition) IsFulfilled(results []map[string][]byte) bool {
return len(results) == 0
}
type IfIndexExistsCondition struct {
ExistsMigrationCondition
TableName string
IndexName string
}
func (c *IfTableExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
return dialect.TableCheckSql(c.TableName)
func (c *IfIndexExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
return dialect.IndexCheckSql(c.TableName, c.IndexName)
}
type IfIndexNotExistsCondition struct {
NotExistsMigrationCondition
TableName string
IndexName string
}
func (c *IfIndexNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
return dialect.IndexCheckSql(c.TableName, c.IndexName)
}
type IfColumnNotExistsCondition struct {
NotExistsMigrationCondition
TableName string
ColumnName string
}
func (c *IfColumnNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
return dialect.ColumnCheckSql(c.TableName, c.ColumnName)
}

View File

@ -29,10 +29,12 @@ type Dialect interface {
DropTable(tableName string) string
DropIndexSql(tableName string, index *Index) string
TableCheckSql(tableName string) (string, []interface{})
RenameTable(oldName string, newName string) string
UpdateTableSql(tableName string, columns []*Column) string
IndexCheckSql(tableName, indexName string) (string, []interface{})
ColumnCheckSql(tableName, columnName string) (string, []interface{})
ColString(*Column) string
ColStringNoPk(*Column) string
@ -182,6 +184,10 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", quote(oldName), quote(newName))
}
func (db *BaseDialect) ColumnCheckSql(tableName, columnName string) (string, []interface{}) {
return "", nil
}
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
quote := db.dialect.Quote
name := index.XName(tableName)

View File

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

View File

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

View File

@ -90,12 +90,6 @@ func (db *Mysql) SqlType(c *Column) string {
return res
}
func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) {
args := []interface{}{"grafana", tableName}
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
return sql, args
}
func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
var statements = []string{}
@ -108,6 +102,18 @@ func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
}
func (db *Mysql) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
args := []interface{}{tableName, indexName}
sql := "SELECT 1 FROM " + db.Quote("INFORMATION_SCHEMA") + "." + db.Quote("STATISTICS") + " WHERE " + db.Quote("TABLE_SCHEMA") + " = DATABASE() AND " + db.Quote("TABLE_NAME") + "=? AND " + db.Quote("INDEX_NAME") + "=?"
return sql, args
}
func (db *Mysql) ColumnCheckSql(tableName, columnName string) (string, []interface{}) {
args := []interface{}{tableName, columnName}
sql := "SELECT 1 FROM " + db.Quote("INFORMATION_SCHEMA") + "." + db.Quote("COLUMNS") + " WHERE " + db.Quote("TABLE_SCHEMA") + " = DATABASE() AND " + db.Quote("TABLE_NAME") + "=? AND " + db.Quote("COLUMN_NAME") + "=?"
return sql, args
}
func (db *Mysql) CleanDB() error {
tables, _ := db.engine.DBMetas()
sess := db.engine.NewSession()

View File

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

View File

@ -68,9 +68,10 @@ func (db *Sqlite3) SqlType(c *Column) string {
}
}
func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) {
args := []interface{}{tableName}
return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
func (db *Sqlite3) IndexCheckSql(tableName, indexName string) (string, []interface{}) {
args := []interface{}{tableName, indexName}
sql := "SELECT 1 FROM " + db.Quote("sqlite_master") + " WHERE " + db.Quote("type") + "='index' AND " + db.Quote("tbl_name") + "=? AND " + db.Quote("name") + "=?"
return sql, args
}
func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string {

View File

@ -243,7 +243,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
ss.dbCfg.Path = filepath.Join(ss.Cfg.DataPath, ss.dbCfg.Path)
}
os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm)
cnnstr = "file:" + ss.dbCfg.Path + "?cache=shared&mode=rwc"
cnnstr = fmt.Sprintf("file:%s?cache=%s&mode=rwc", ss.dbCfg.Path, ss.dbCfg.CacheMode)
default:
return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type)
}
@ -319,6 +319,8 @@ func (ss *SqlStore) readConfig() {
ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String()
ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String()
ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db")
ss.dbCfg.CacheMode = sec.Key("cache_mode").MustString("private")
}
func InitTestDB(t *testing.T) *SqlStore {
@ -391,7 +393,13 @@ func IsTestDbPostgres() bool {
}
type DatabaseConfig struct {
Type, Host, Name, User, Pwd, Path, SslMode string
Type string
Host string
Name string
User string
Pwd string
Path string
SslMode string
CaCertPath string
ClientKeyPath string
ClientCertPath string
@ -400,4 +408,5 @@ type DatabaseConfig struct {
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("")
useRawQuery := model.Get("rawQuery").MustBool(false)
alias := model.Get("alias").MustString("")
tz := model.Get("tz").MustString("")
measurement := model.Get("measurement").MustString("")
@ -55,6 +56,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data
Interval: parsedInterval,
Alias: alias,
UseRawQuery: useRawQuery,
Tz: tz,
}, nil
}

View File

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

View File

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

View File

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

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)`)
})
Convey("can build query with tz", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},
Measurement: "cpu",
GroupBy: []*QueryPart{groupBy1},
Tz: "Europe/Paris",
Interval: time.Second * 5,
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(5s) tz('Europe/Paris')`)
})
Convey("can build query with group bys", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},

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.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":
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil
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":
if len(args) < 2 {
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)")
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() {
@ -138,7 +138,7 @@ func TestMacroEngine(t *testing.T) {
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.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() {
@ -158,7 +158,7 @@ func TestMacroEngine(t *testing.T) {
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.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() {
@ -168,5 +168,22 @@ func TestMacroEngine(t *testing.T) {
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 { Label } from './Label';

View File

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

View File

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

View File

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

View File

@ -2,14 +2,23 @@ import _ from 'lodash';
import { TimeSeries } from 'app/core/core';
import colors, { getThemeColor } from 'app/core/utils/colors';
/**
* Mapping of log level abbreviation to canonical log level.
* Supported levels are reduce to limit color variation.
*/
export enum LogLevel {
emerg = 'critical',
alert = 'critical',
crit = 'critical',
critical = 'critical',
warn = 'warning',
warning = 'warning',
err = 'error',
eror = 'error',
error = 'error',
info = 'info',
notice = 'info',
dbug = 'debug',
debug = 'debug',
trace = 'trace',
unkown = 'unkown',
@ -81,7 +90,9 @@ export interface LogsStream {
export interface LogsStreamEntry {
line: string;
timestamp: string;
ts: string;
// Legacy, was renamed to ts
timestamp?: string;
}
export interface LogsStreamLabels {

View File

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

View File

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

View File

@ -629,6 +629,8 @@ kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³');
kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³');
kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³');
kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³');
kbn.valueFormats.conmgdL = kbn.formatBuilders.fixedUnit('mg/dL');
kbn.valueFormats.conmmolL = kbn.formatBuilders.fixedUnit('mmol/L');
// Time
kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
@ -1209,6 +1211,8 @@ kbn.getUnitFormats = () => {
{ text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' },
{ text: 'gram per cubic meter (g/m³)', value: 'congm3' },
{ text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' },
{ text: 'milligrams per decilitre (mg/dL)', value: 'conmgdL' },
{ text: 'millimoles per litre (mmol/L)', value: 'conmmolL' },
],
},
];

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,8 @@ import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource
import kbn from 'app/core/utils/kbn';
// Types
import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types';
import { DataQueryOptions, DataQueryResponse } from 'app/types';
import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui';
interface RenderProps {
loading: LoadingState;

View File

@ -16,7 +16,8 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
// Types
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelPlugin, TimeRange } from 'app/types';
import { PanelPlugin } from 'app/types';
import { TimeRange } from '@grafana/ui';
export interface Props {
panel: PanelModel;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@
</div>
<div ng-if="ctrl.dashboard.links.length > 0" >
<dash-links-container links="ctrl.dashboard.links" class="gf-form-inline"></dash-links-container>
<dash-links-container links="ctrl.dashboard.links" dashboard="ctrl.dashboard" class="gf-form-inline"></dash-links-container>
</div>
<div class="clearfix"></div>

View File

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

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 { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelMenuItem } from 'app/types/panel';
import { PanelMenuItem } from '@grafana/ui';
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
const onViewPanel = () => {

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import 'vendor/flot/jquery.flot.time';
import 'vendor/flot/jquery.flot.selection';
import 'vendor/flot/jquery.flot.stack';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
import * as dateMath from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';

View File

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

View File

@ -3,7 +3,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
import { Emitter } from 'app/core/utils/emitter';
import { getIntervals } from 'app/core/utils/explore';
import { DataQuery } from 'app/types';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange } from '@grafana/ui';
import { getTimeSrv } from 'app/features/dashboard/time_srv';
import 'app/features/plugins/plugin_loader';

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, TimeRange } from 'app/types/series';
import { RawTimeRange, TimeRange } from '@grafana/ui';
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export const DEFAULT_RANGE = {

View File

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

View File

@ -22,7 +22,7 @@
<div class="panel-option-section__body">
<div class="section">
<div class="gf-form">
<span class="gf-form-label width-9">Repat</span>
<span class="gf-form-label width-9">Repeat</span>
<dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat">
@ -42,7 +42,7 @@
</div>
<div class="panel-option-section">
<div class="panel-option-section__header">Drildown Links</div>
<div class="panel-option-section__header">Drilldown Links</div>
<div class="panel-option-section__body">
<panel-links-editor panel="ctrl.panel"></panel-links-editor>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -119,6 +119,16 @@
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.target.tz">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">tz</label>
<input type="text" class="gf-form-input width-9" ng-model="ctrl.target.tz" spellcheck='false' placeholder="No Timezone" ng-blur="ctrl.refresh()">
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">FORMAT AS</label>

View File

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

View File

@ -2,14 +2,23 @@ import React from 'react';
const CHEAT_SHEET_ITEMS = [
{
title: 'Logs From a Job',
title: 'See your logs',
label: 'Start by selecting a log stream from the Log labels selector.',
},
{
title: 'Logs from a "job"',
expression: '{job="default/prometheus"}',
label: 'Returns all log lines emitted by instances of this job.',
},
{
title: 'Search For Text',
expression: '{app="cassandra"} Maximum memory usage',
label: 'Returns all log lines for the selector and highlights the given text in the results.',
title: 'Combine stream selectors',
expression: '{app="cassandra",namespace="prod"}',
label: 'Returns all log lines from streams that have both labels.',
},
{
title: 'Search for text',
expression: '{app="cassandra"} (duration|latency)\\s*(=|is|of)\\s*[\\d\\.]+',
label: 'Add a regular expression after the selector to filter for.',
},
];
@ -19,12 +28,14 @@ export default (props: any) => (
{CHEAT_SHEET_ITEMS.map(item => (
<div className="cheat-sheet-item" key={item.expression}>
<div className="cheat-sheet-item__title">{item.title}</div>
{item.expression && (
<div
className="cheat-sheet-item__expression"
onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
>
<code>{item.expression}</code>
</div>
)}
<div className="cheat-sheet-item__label">{item.label}</div>
</div>
))}

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