浏览代码

Merge pull request #5 from grafana/master

Update master
Pavel 7 年之前
父节点
当前提交
ea4223f923
共有 100 个文件被更改,包括 890 次插入266 次删除
  1. 2 1
      CHANGELOG.md
  2. 3 0
      conf/defaults.ini
  3. 3 0
      conf/sample.ini
  4. 7 0
      devenv/docker/blocks/alert_webhook_listener/Dockerfile
  5. 5 0
      devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml
  6. 24 0
      devenv/docker/blocks/alert_webhook_listener/main.go
  7. 2 1
      docs/sources/auth/auth-proxy.md
  8. 6 0
      docs/sources/installation/configuration.md
  9. 3 1
      jest.config.js
  10. 41 16
      package.json
  11. 4 0
      packages/grafana-build/README.md
  12. 13 0
      packages/grafana-build/package.json
  13. 3 0
      packages/grafana-ui/README.md
  14. 33 0
      packages/grafana-ui/package.json
  15. 5 4
      packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx
  16. 10 10
      packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx
  17. 0 0
      packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss
  18. 1 0
      packages/grafana-ui/src/components/index.scss
  19. 1 0
      packages/grafana-ui/src/components/index.ts
  20. 23 0
      packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx
  21. 1 0
      packages/grafana-ui/src/forms/index.ts
  22. 1 0
      packages/grafana-ui/src/index.scss
  23. 5 0
      packages/grafana-ui/src/index.ts
  24. 3 0
      packages/grafana-ui/src/types/index.ts
  25. 17 0
      packages/grafana-ui/src/types/jquery.d.ts
  26. 31 0
      packages/grafana-ui/src/types/panel.ts
  27. 53 0
      packages/grafana-ui/src/types/series.ts
  28. 17 0
      packages/grafana-ui/src/types/time.ts
  29. 1 0
      packages/grafana-ui/src/utils/index.ts
  30. 174 0
      packages/grafana-ui/src/utils/processTimeSeries.ts
  31. 8 6
      packages/grafana-ui/src/visualizations/Graph/Graph.tsx
  32. 1 0
      packages/grafana-ui/src/visualizations/index.ts
  33. 18 0
      packages/grafana-ui/tsconfig.json
  34. 3 0
      packages/grafana-ui/tslint.json
  35. 2 0
      pkg/login/ldap.go
  36. 3 1
      pkg/services/dashboards/dashboard_service.go
  37. 5 1
      pkg/services/notifications/webhook.go
  38. 27 12
      pkg/services/session/mysql.go
  39. 38 3
      pkg/services/sqlstore/migrator/conditions.go
  40. 7 1
      pkg/services/sqlstore/migrator/dialect.go
  41. 9 13
      pkg/services/sqlstore/migrator/migrations.go
  42. 17 7
      pkg/services/sqlstore/migrator/migrator.go
  43. 12 6
      pkg/services/sqlstore/migrator/mysql_dialect.go
  44. 3 3
      pkg/services/sqlstore/migrator/postgres_dialect.go
  45. 4 3
      pkg/services/sqlstore/migrator/sqlite_dialect.go
  46. 19 10
      pkg/services/sqlstore/sqlstore.go
  47. 2 0
      pkg/tsdb/influxdb/model_parser.go
  48. 2 0
      pkg/tsdb/influxdb/model_parser_test.go
  49. 1 0
      pkg/tsdb/influxdb/models.go
  50. 10 0
      pkg/tsdb/influxdb/query.go
  51. 14 0
      pkg/tsdb/influxdb/query_test.go
  52. 3 3
      pkg/tsdb/postgres/macros.go
  53. 20 3
      pkg/tsdb/postgres/macros_test.go
  54. 0 43
      public/app/core/components/Form/Element.tsx
  55. 0 19
      public/app/core/components/Form/Label.tsx
  56. 0 2
      public/app/core/components/Form/index.ts
  57. 5 1
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  58. 2 2
      public/app/core/directives/tags.ts
  59. 1 1
      public/app/core/live/live_srv.ts
  60. 12 1
      public/app/core/logs_model.ts
  61. 4 5
      public/app/core/utils/explore.test.ts
  62. 3 2
      public/app/core/utils/explore.ts
  63. 4 0
      public/app/core/utils/kbn.ts
  64. 1 1
      public/app/core/utils/rangeutil.ts
  65. 3 3
      public/app/features/alerting/AlertRuleItem.tsx
  66. 2 2
      public/app/features/api-keys/ApiKeysPage.tsx
  67. 3 3
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  68. 2 1
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  69. 2 1
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  70. 1 1
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx
  71. 1 1
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx
  72. 5 1
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  73. 4 3
      public/app/features/dashboard/dashgrid/PanelResizer.tsx
  74. 2 1
      public/app/features/dashboard/dashgrid/QueryOptions.tsx
  75. 4 1
      public/app/features/dashboard/dashlinks/module.ts
  76. 1 1
      public/app/features/dashboard/submenu/submenu.html
  77. 2 2
      public/app/features/dashboard/time_srv.ts
  78. 1 1
      public/app/features/dashboard/utils/getPanelMenu.ts
  79. 1 1
      public/app/features/dashboard/utils/panel.ts
  80. 1 1
      public/app/features/datasources/DataSourcesList.tsx
  81. 17 7
      public/app/features/explore/Explore.tsx
  82. 1 1
      public/app/features/explore/Graph.tsx
  83. 1 1
      public/app/features/explore/Logs.tsx
  84. 1 1
      public/app/features/explore/QueryEditor.tsx
  85. 9 2
      public/app/features/explore/QueryField.tsx
  86. 1 1
      public/app/features/explore/QueryRows.tsx
  87. 1 1
      public/app/features/explore/TimePicker.tsx
  88. 4 5
      public/app/features/panel/panel_ctrl.ts
  89. 2 2
      public/app/features/panel/partials/general_tab.html
  90. 1 1
      public/app/features/plugins/PluginList.tsx
  91. 3 8
      public/app/features/plugins/plugin_loader.ts
  92. 2 2
      public/app/features/teams/TeamList.tsx
  93. 2 2
      public/app/features/teams/TeamMembers.tsx
  94. 5 5
      public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
  95. 10 10
      public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap
  96. 1 1
      public/app/features/users/UsersActionBar.tsx
  97. 4 0
      public/app/plugins/datasource/influxdb/influx_query.ts
  98. 10 0
      public/app/plugins/datasource/influxdb/partials/query.editor.html
  99. 7 0
      public/app/plugins/datasource/influxdb/query_ctrl.ts
  100. 21 10
      public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx

+ 2 - 1
CHANGELOG.md

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

+ 3 - 0
conf/defaults.ini

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

+ 3 - 0
conf/sample.ini

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

+ 7 - 0
devenv/docker/blocks/alert_webhook_listener/Dockerfile

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

+ 5 - 0
devenv/docker/blocks/alert_webhook_listener/docker-compose.yaml

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

+ 24 - 0
devenv/docker/blocks/alert_webhook_listener/main.go

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

+ 2 - 1
docs/sources/auth/auth-proxy.md

@@ -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 =
 ```
 

+ 6 - 0
docs/sources/installation/configuration.md

@@ -250,6 +250,12 @@ Sets the maximum amount of time a connection may be reused. The default is 14400
 
 Set to `true` to log the sql calls and execution times.
 
+### cache_mode
+
+For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared)
+Defaults to private.
+
+
 <hr />
 
 ## [security]

+ 3 - 1
jest.config.js

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

+ 41 - 16
package.json

@@ -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/*/**"
+    ]
   }
 }

+ 4 - 0
packages/grafana-build/README.md

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

+ 13 - 0
packages/grafana-build/package.json

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

+ 3 - 0
packages/grafana-ui/README.md

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

+ 33 - 0
packages/grafana-ui/package.json

@@ -0,0 +1,33 @@
+{
+  "name": "@grafana/ui",
+  "version": "1.0.0",
+  "description": "",
+  "main": "src/index.ts",
+  "scripts": {
+    "tslint": "tslint -c tslint.json --project tsconfig.json",
+    "typecheck": "tsc --noEmit"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@torkelo/react-select": "2.1.1",
+    "classnames": "^2.2.5",
+    "jquery": "^3.2.1",
+    "lodash": "^4.17.10",
+    "moment": "^2.22.2",
+    "react": "^16.6.3",
+    "react-dom": "^16.6.3",
+    "react-highlight-words": "0.11.0",
+    "react-popper": "^1.3.0",
+    "react-transition-group": "^2.2.1",
+    "react-virtualized": "^9.21.0"
+  },
+  "devDependencies": {
+    "@types/jest": "^23.3.2",
+    "@types/lodash": "^4.14.119",
+    "@types/react": "^16.7.6",
+    "@types/classnames": "^2.2.6",
+    "@types/jquery": "^1.10.35",
+    "typescript": "^3.2.2"
+  }
+}

+ 5 - 4
public/app/core/components/DeleteButton/DeleteButton.test.tsx → packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx

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

+ 10 - 10
public/app/core/components/DeleteButton/DeleteButton.tsx → packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx

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

+ 0 - 0
public/sass/components/_delete_button.scss → packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss


+ 1 - 0
packages/grafana-ui/src/components/index.scss

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

+ 1 - 0
packages/grafana-ui/src/components/index.ts

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

+ 23 - 0
packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx

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

+ 1 - 0
packages/grafana-ui/src/forms/index.ts

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

+ 1 - 0
packages/grafana-ui/src/index.scss

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

+ 5 - 0
packages/grafana-ui/src/index.ts

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

+ 3 - 0
packages/grafana-ui/src/types/index.ts

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

+ 17 - 0
packages/grafana-ui/src/types/jquery.d.ts

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

+ 31 - 0
packages/grafana-ui/src/types/panel.ts

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

+ 53 - 0
packages/grafana-ui/src/types/series.ts

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

+ 17 - 0
packages/grafana-ui/src/types/time.ts

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

+ 1 - 0
packages/grafana-ui/src/utils/index.ts

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

+ 174 - 0
packages/grafana-ui/src/utils/processTimeSeries.ts

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

+ 8 - 6
public/app/viz/Graph.tsx → packages/grafana-ui/src/visualizations/Graph/Graph.tsx

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

+ 1 - 0
packages/grafana-ui/src/visualizations/index.ts

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

+ 18 - 0
packages/grafana-ui/tsconfig.json

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

+ 3 - 0
packages/grafana-ui/tslint.json

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

+ 2 - 0
pkg/login/ldap.go

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

+ 3 - 1
pkg/services/dashboards/dashboard_service.go

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

+ 5 - 1
pkg/services/notifications/webhook.go

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

+ 27 - 12
pkg/services/session/mysql.go

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

+ 38 - 3
pkg/services/sqlstore/migrator/conditions.go

@@ -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 *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 *IfTableExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
-	return dialect.TableCheckSql(c.TableName)
+func (c *IfColumnNotExistsCondition) Sql(dialect Dialect) (string, []interface{}) {
+	return dialect.ColumnCheckSql(c.TableName, c.ColumnName)
 }

+ 7 - 1
pkg/services/sqlstore/migrator/dialect.go

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

+ 9 - 13
pkg/services/sqlstore/migrator/migrations.go

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

+ 17 - 7
pkg/services/sqlstore/migrator/migrator.go

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

+ 12 - 6
pkg/services/sqlstore/migrator/mysql_dialect.go

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

+ 3 - 3
pkg/services/sqlstore/migrator/postgres_dialect.go

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

+ 4 - 3
pkg/services/sqlstore/migrator/sqlite_dialect.go

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

+ 19 - 10
pkg/services/sqlstore/sqlstore.go

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

+ 2 - 0
pkg/tsdb/influxdb/model_parser.go

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

+ 2 - 0
pkg/tsdb/influxdb/model_parser_test.go

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

+ 1 - 0
pkg/tsdb/influxdb/models.go

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

+ 10 - 0
pkg/tsdb/influxdb/query.go

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

+ 14 - 0
pkg/tsdb/influxdb/query_test.go

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

+ 3 - 3
pkg/tsdb/postgres/macros.go

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

+ 20 - 3
pkg/tsdb/postgres/macros_test.go

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

+ 0 - 43
public/app/core/components/Form/Element.tsx

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

+ 0 - 19
public/app/core/components/Form/Label.tsx

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

+ 0 - 2
public/app/core/components/Form/index.ts

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

+ 5 - 1
public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx

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

+ 2 - 2
public/app/core/directives/tags.ts

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

+ 1 - 1
public/app/core/live/live_srv.ts

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

+ 12 - 1
public/app/core/logs_model.ts

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

+ 4 - 5
public/app/core/utils/explore.test.ts

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

+ 3 - 2
public/app/core/utils/explore.ts

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

+ 4 - 0
public/app/core/utils/kbn.ts

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

+ 1 - 1
public/app/core/utils/rangeutil.ts

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

+ 3 - 3
public/app/features/alerting/AlertRuleItem.tsx

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

+ 2 - 2
public/app/features/api-keys/ApiKeysPage.tsx

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

+ 3 - 3
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

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

+ 2 - 1
public/app/features/dashboard/dashgrid/DataPanel.tsx

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

+ 2 - 1
public/app/features/dashboard/dashgrid/PanelChrome.tsx

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

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx

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

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx

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

+ 5 - 1
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

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

+ 4 - 3
public/app/features/dashboard/dashgrid/PanelResizer.tsx

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

+ 2 - 1
public/app/features/dashboard/dashgrid/QueryOptions.tsx

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

+ 4 - 1
public/app/features/dashboard/dashlinks/module.ts

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

+ 1 - 1
public/app/features/dashboard/submenu/submenu.html

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

+ 2 - 2
public/app/features/dashboard/time_srv.ts

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

+ 1 - 1
public/app/features/dashboard/utils/getPanelMenu.ts

@@ -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 = () => {

+ 1 - 1
public/app/features/dashboard/utils/panel.ts

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

+ 1 - 1
public/app/features/datasources/DataSourcesList.tsx

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

+ 17 - 7
public/app/features/explore/Explore.tsx

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

+ 1 - 1
public/app/features/explore/Graph.tsx

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

+ 1 - 1
public/app/features/explore/Logs.tsx

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

+ 1 - 1
public/app/features/explore/QueryEditor.tsx

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

+ 9 - 2
public/app/features/explore/QueryField.tsx

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

+ 1 - 1
public/app/features/explore/QueryRows.tsx

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

+ 1 - 1
public/app/features/explore/TimePicker.tsx

@@ -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 = {

+ 4 - 5
public/app/features/panel/panel_ctrl.ts

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

+ 2 - 2
public/app/features/panel/partials/general_tab.html

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

+ 1 - 1
public/app/features/plugins/PluginList.tsx

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

+ 3 - 8
public/app/features/plugins/plugin_loader.ts

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

+ 2 - 2
public/app/features/teams/TeamList.tsx

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

+ 2 - 2
public/app/features/teams/TeamMembers.tsx

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

+ 5 - 5
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap

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

+ 10 - 10
public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap

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

+ 1 - 1
public/app/features/users/UsersActionBar.tsx

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

+ 4 - 0
public/app/plugins/datasource/influxdb/influx_query.ts

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

+ 10 - 0
public/app/plugins/datasource/influxdb/partials/query.editor.html

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

+ 7 - 0
public/app/plugins/datasource/influxdb/query_ctrl.ts

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

+ 21 - 10
public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx

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

部分文件因为文件数量过多而无法显示