Преглед изворни кода

Merge remote-tracking branch 'grafana/master'

* grafana/master: (54 commits)
  s/initialDatasourceId/initialDatasource/
  Fixed issues with panel size in edit mode, fixes #14703
  Tweak datetime picker layout for mobile
  Explore: Remember last use datasource
  Update yarn.lock
  Logs data model: add more log levels
  Review feedback
  Explore: fix loading indicator z-index on panel container
  Loki: change query row to be single field again
  Explore: logging UI style fixes
  Loki: query limit configurable in datasource
  Removed rxjs compat
  ldap: adds extra debug logging
  adds orgId to user dto for provisioned dashboards
  Update rxjs
  closes the body properly on successful webhooks
  makes cache mode configurable
  Fix general tab typos
  added node-sass as dev dependency, needed after I removed grunt-sass
  Husky and sasslint fixes, fixes #14638
  ...
ryan пре 7 година
родитељ
комит
6435df0a22
100 измењених фајлова са 914 додато и 262 уклоњено
  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. 2 0
      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. 0 43
      public/app/core/components/Form/Element.tsx
  53. 0 19
      public/app/core/components/Form/Label.tsx
  54. 0 2
      public/app/core/components/Form/index.ts
  55. 5 1
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  56. 2 2
      public/app/core/directives/tags.ts
  57. 1 1
      public/app/core/live/live_srv.ts
  58. 12 1
      public/app/core/logs_model.ts
  59. 4 5
      public/app/core/utils/explore.test.ts
  60. 3 2
      public/app/core/utils/explore.ts
  61. 4 0
      public/app/core/utils/kbn.ts
  62. 1 1
      public/app/core/utils/rangeutil.ts
  63. 3 3
      public/app/features/alerting/AlertRuleItem.tsx
  64. 2 2
      public/app/features/api-keys/ApiKeysPage.tsx
  65. 3 3
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  66. 2 1
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  67. 2 1
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  68. 1 1
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx
  69. 1 1
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx
  70. 5 1
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  71. 4 3
      public/app/features/dashboard/dashgrid/PanelResizer.tsx
  72. 2 1
      public/app/features/dashboard/dashgrid/QueryOptions.tsx
  73. 4 1
      public/app/features/dashboard/dashlinks/module.ts
  74. 1 1
      public/app/features/dashboard/submenu/submenu.html
  75. 2 2
      public/app/features/dashboard/time_srv.ts
  76. 1 1
      public/app/features/dashboard/utils/getPanelMenu.ts
  77. 1 1
      public/app/features/dashboard/utils/panel.ts
  78. 1 1
      public/app/features/datasources/DataSourcesList.tsx
  79. 17 7
      public/app/features/explore/Explore.tsx
  80. 1 1
      public/app/features/explore/Graph.tsx
  81. 1 1
      public/app/features/explore/Logs.tsx
  82. 1 1
      public/app/features/explore/QueryEditor.tsx
  83. 9 2
      public/app/features/explore/QueryField.tsx
  84. 1 1
      public/app/features/explore/QueryRows.tsx
  85. 1 1
      public/app/features/explore/TimePicker.tsx
  86. 4 5
      public/app/features/panel/panel_ctrl.ts
  87. 2 2
      public/app/features/panel/partials/general_tab.html
  88. 1 1
      public/app/features/plugins/PluginList.tsx
  89. 3 8
      public/app/features/plugins/plugin_loader.ts
  90. 2 2
      public/app/features/teams/TeamList.tsx
  91. 2 2
      public/app/features/teams/TeamMembers.tsx
  92. 5 5
      public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
  93. 10 10
      public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap
  94. 1 1
      public/app/features/users/UsersActionBar.tsx
  95. 4 0
      public/app/plugins/datasource/influxdb/influx_query.ts
  96. 10 0
      public/app/plugins/datasource/influxdb/partials/query.editor.html
  97. 7 0
      public/app/plugins/datasource/influxdb/query_ctrl.ts
  98. 21 10
      public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
  99. 18 2
      public/app/plugins/datasource/loki/components/LokiQueryField.tsx
  100. 30 1
      public/app/plugins/datasource/loki/datasource.test.ts

+ 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

+ 2 - 0
pkg/services/dashboards/dashboard_service.go

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

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

+ 18 - 2
public/app/plugins/datasource/loki/components/LokiQueryField.tsx

@@ -15,6 +15,16 @@ import { DataQuery } from 'app/types';
 
 const PRISM_SYNTAX = 'promql';
 
+function getChooserText(hasSytax, hasLogLabels) {
+  if (!hasSytax) {
+    return 'Loading labels...';
+  }
+  if (!hasLogLabels) {
+    return '(No labels found)';
+  }
+  return 'Log labels';
+}
+
 export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
   // Modify suggestion based on context
   switch (typeaheadContext) {
@@ -67,7 +77,10 @@ interface LokiQueryFieldState {
 
 class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
   plugins: any[];
+  pluginsSearch: any[];
   languageProvider: any;
+  modifiedSearch: string;
+  modifiedQuery: string;
 
   constructor(props: LokiQueryFieldProps, context) {
     super(props, context);
@@ -85,6 +98,8 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
       }),
     ];
 
+    this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
+
     this.state = {
       logLabelOptions: [],
       syntaxLoaded: false,
@@ -189,7 +204,8 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
     const { error, hint, initialQuery } = this.props;
     const { logLabelOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
-    const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
+    const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
+    const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
 
     return (
       <div className="prom-query-field">
@@ -208,7 +224,7 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
             onTypeahead={this.onTypeahead}
             onWillApplySuggestion={willApplySuggestion}
             onValueChanged={this.onChangeQuery}
-            placeholder="Enter a Loki Log query"
+            placeholder="Enter a Loki query"
             portalOrigin="loki"
             syntaxLoaded={syntaxLoaded}
           />

+ 30 - 1
public/app/plugins/datasource/loki/datasource.test.ts

@@ -1,10 +1,39 @@
 import LokiDatasource from './datasource';
 
 describe('LokiDatasource', () => {
-  const instanceSettings = {
+  const instanceSettings: any = {
     url: 'myloggingurl',
   };
 
+  describe('when querying', () => {
+    const backendSrvMock = { datasourceRequest: jest.fn() };
+
+    const templateSrvMock = {
+      getAdhocFilters: () => [],
+      replace: a => a,
+    };
+
+    const range = { from: 'now-6h', to: 'now' };
+
+    test('should use default max lines when no limit given', () => {
+      const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
+      backendSrvMock.datasourceRequest = jest.fn();
+      ds.query({ range, targets: [{ expr: 'foo' }] });
+      expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
+      expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=1000');
+    });
+
+    test('should use custom max lines if limit is set', () => {
+      const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
+      const customSettings = { ...instanceSettings, jsonData: customData };
+      const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
+      backendSrvMock.datasourceRequest = jest.fn();
+      ds.query({ range, targets: [{ expr: 'foo' }] });
+      expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
+      expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
+    });
+  });
+
   describe('when performing testDataSource', () => {
     let ds;
     let result;

Неке датотеке нису приказане због велике количине промена