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)
@ -24,7 +25,7 @@
* **Datasource admin**: Fix for issue creating new data source when same name exists [#14467](https://github.com/grafana/grafana/issues/14467)
* **OAuth**: Fix for oauth auto login setting, can now be set using env variable [#14435](https://github.com/grafana/grafana/issues/14435)
* **Dashboard search**: Fix for searching tags in tags filter dropdown.
* **Dashboard search**: Fix for searching tags in tags filter dropdown.
# 5.4.1 (2018-12-10)

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

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

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,13 +393,20 @@ func IsTestDbPostgres() bool {
}
type DatabaseConfig struct {
Type, Host, Name, User, Pwd, Path, SslMode string
CaCertPath string
ClientKeyPath string
ClientCertPath string
ServerCertName string
ConnectionString string
MaxOpenConn int
MaxIdleConn int
ConnMaxLifetime int
Type string
Host string
Name string
User string
Pwd string
Path string
SslMode string
CaCertPath string
ClientKeyPath string
ClientCertPath string
ServerCertName string
ConnectionString string
MaxOpenConn int
MaxIdleConn int
ConnMaxLifetime int
CacheMode string
}

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>
<div
className="cheat-sheet-item__expression"
onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
>
<code>{item.expression}</code>
</div>
{item.expression && (
<div
className="cheat-sheet-item__expression"
onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
>
<code>{item.expression}</code>
</div>
)}
<div className="cheat-sheet-item__label">{item.label}</div>
</div>
))}

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