diff --git a/.circleci/config.yml b/.circleci/config.yml index dba6c5f8bd0..7f9c40bd968 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,7 +127,7 @@ jobs: build-all: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -200,51 +200,51 @@ jobs: - dist/grafana* grafana-docker-master: - docker: - - image: docker:stable-git + machine: + image: circleci/classic:201808-01 steps: - checkout - attach_workspace: at: . - - setup_remote_docker - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" - - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz + - run: rm packaging/docker/grafana-latest.linux-*.tar.gz - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - run: cd packaging/docker && ./build-enterprise.sh "master" grafana-docker-pr: - docker: - - image: docker:stable-git + machine: + image: circleci/classic:201808-01 steps: - checkout - attach_workspace: at: . - - setup_remote_docker - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}" grafana-docker-release: - docker: - - image: docker:stable-git - steps: - - checkout - - attach_workspace: - at: . - - setup_remote_docker - - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" - - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz - - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" + machine: + image: circleci/classic:201808-01 + steps: + - checkout + - attach_workspace: + at: . + - run: docker info + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker + - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" + - run: rm packaging/docker/grafana-latest.linux-*.tar.gz + - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" build-enterprise: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -276,7 +276,7 @@ jobs: build-all-enterprise: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -323,7 +323,7 @@ jobs: deploy-enterprise-master: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -346,7 +346,7 @@ jobs: deploy-enterprise-release: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -365,10 +365,20 @@ jobs: - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh --enterprise' + - run: + name: Load GPG private key + comand: './scripts/build/load-signing-key.sh' + - run: + name: Update Debian repository + command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + - run: + name: Update RPM repository + command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + deploy-master: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -398,8 +408,9 @@ jobs: deploy-release: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: + - checkout - attach_workspace: at: . - run: @@ -417,6 +428,15 @@ jobs: - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh' + - run: + name: Load GPG private key + comand: './scripts/build/load-signing-key.sh' + - run: + name: Update Debian repository + command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + - run: + name: Update RPM repository + command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' workflows: version: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 671740f7225..46b7381cba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * **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) * **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok) +* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach) ### Bug fixes * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) diff --git a/README.md b/README.md index 1ce4ffbe109..ff5da04f209 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,9 @@ GRAFANA_TEST_DB=postgres go test ./pkg/... If you have any idea for an improvement or found a bug, do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana -the kickass metrics & devops dashboard we all dream about! +the kickass metrics & devops dashboard we all dream about! + +Read the [contributing](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) guide then check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are easy and that we would like help with. ## Plugin development diff --git a/build.go b/build.go index 9d5216de1d0..4486cd3deb9 100644 --- a/build.go +++ b/build.go @@ -164,6 +164,8 @@ func makeLatestDistCopies() { "_amd64.deb": "dist/grafana_latest_amd64.deb", ".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm", ".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz", + ".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz", + ".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz", } for _, file := range files { diff --git a/devenv/docker/blocks/influxdb/influxdb.conf b/devenv/docker/blocks/influxdb/influxdb.conf index c0331ce7449..120739dd896 100644 --- a/devenv/docker/blocks/influxdb/influxdb.conf +++ b/devenv/docker/blocks/influxdb/influxdb.conf @@ -69,6 +69,7 @@ reporting-disabled = false unix-socket-enabled = false # enable http service over unix domain socket # bind-socket = "/var/run/influxdb.sock" + flux-enabled = true [subscriber] enabled = true diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 6be12600da5..3d96923bc72 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized "list": [] }, "refresh": "5s", - "schemaVersion": 16, + "schemaVersion": 17, "version": 0, "links": [] } diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index f20cc0ccfc9..71ce6bdd2ae 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -292,9 +292,11 @@ The `direction` controls how the panels will be arranged. By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated -panel. Each panel will never be smaller that the provided `Min width` if you have many selected values. +panel. -By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. +Set `Max per row` to tell grafana how many panels per row you want at most. It defaults to *4* if you don't set anything. + +By choosing `vertical` the panels will be arranged from top to bottom in a column. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build. You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard. diff --git a/package.json b/package.json index eefe2cbbe53..470101ff0c4 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@types/jquery": "^1.10.35", "@types/node": "^8.0.31", "@types/react": "^16.7.6", - "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.9", "@types/react-select": "^2.0.4", "angular-mocks": "1.6.6", @@ -65,6 +64,7 @@ "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^23.6.0", + "jest-date-mock": "^1.0.6", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", @@ -72,8 +72,8 @@ "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", - "npm": "^5.4.2", "node-sass": "^4.11.0", + "npm": "^5.4.2", "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", @@ -167,7 +167,6 @@ "prop-types": "^15.6.2", "rc-cascader": "^0.14.0", "react": "^16.6.3", - "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-grid-layout": "0.16.6", "react-highlight-words": "0.11.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 31a3168d4a7..0801d976eac 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -17,11 +17,15 @@ "lodash": "^4.17.10", "moment": "^2.22.2", "react": "^16.6.3", + "react-custom-scrollbars": "^4.2.1", "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" + "react-virtualized": "^9.21.0", + "tether": "^1.4.0", + "tether-drop": "https://github.com/torkelo/drop/tarball/master", + "tinycolor2": "^1.4.1" }, "devDependencies": { "@storybook/addon-info": "^4.1.4", @@ -33,8 +37,14 @@ "@types/node": "^10.12.18", "@types/react": "^16.7.6", "@types/storybook__react": "^4.0.0", + "@types/react-custom-scrollbars": "^4.0.5", + "@types/react-test-renderer": "^16.0.3", + "@types/react-transition-group": "^2.0.15", + "@types/tether-drop": "^1.4.8", + "@types/tinycolor2": "^1.4.1", "awesome-typescript-loader": "^5.2.1", "react-docgen-typescript-webpack-plugin": "^1.1.0", + "react-test-renderer": "^16.7.0", "typescript": "^3.2.2" }, "resolutions": { diff --git a/public/app/core/specs/ColorPalette.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx similarity index 80% rename from public/app/core/specs/ColorPalette.test.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx index fb1124aa975..0714180de54 100644 --- a/public/app/core/specs/ColorPalette.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import { ColorPalette } from '../components/colorpicker/ColorPalette'; +import { ColorPalette } from './ColorPalette'; describe('CollorPalette', () => { it('renders correctly', () => { diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx similarity index 90% rename from public/app/core/components/colorpicker/ColorPalette.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx index edb2629d16d..03ed9949361 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { sortedColors } from 'app/core/utils/colors'; +import { sortedColors } from '../../utils'; export interface Props { color: string; @@ -9,13 +9,13 @@ export interface Props { export class ColorPalette extends React.Component { paletteColors: string[]; - constructor(props) { + constructor(props: Props) { super(props); this.paletteColors = sortedColors; this.onColorSelect = this.onColorSelect.bind(this); } - onColorSelect(color) { + onColorSelect(color: string) { return () => { this.props.onColorSelect(color); }; diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx similarity index 83% rename from public/app/core/components/colorpicker/ColorPicker.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 9541001b0a8..485aa5f03d3 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Drop from 'tether-drop'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface Props { color: string; @@ -10,7 +9,7 @@ export interface Props { } export class ColorPicker extends React.Component { - pickerElem: HTMLElement; + pickerElem: HTMLElement | null; colorPickerDrop: any; openColorPicker = () => { @@ -20,7 +19,7 @@ export class ColorPicker extends React.Component { ReactDOM.render(dropContent, dropContentElem); const drop = new Drop({ - target: this.pickerElem, + target: this.pickerElem as Element, content: dropContentElem, position: 'top center', classes: 'drop-popover', @@ -28,6 +27,7 @@ export class ColorPicker extends React.Component { hoverCloseDelay: 200, tetherOptions: { constraints: [{ to: 'scrollParent', attachment: 'none both' }], + attachment: 'bottom center', }, }); @@ -45,7 +45,7 @@ export class ColorPicker extends React.Component { }, 100); }; - onColorSelect = color => { + onColorSelect = (color: string) => { this.props.onChange(color); }; @@ -59,8 +59,3 @@ export class ColorPicker extends React.Component { ); } } - -react2AngularDirective('colorPicker', ColorPicker, [ - 'color', - ['onChange', { watchDepth: 'reference', wrapApply: true }], -]); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx similarity index 83% rename from public/app/core/components/colorpicker/ColorPickerPopover.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index c42bcfa1d06..e8305c99319 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -14,7 +14,7 @@ export interface Props { export class ColorPickerPopover extends React.Component { pickerNavElem: any; - constructor(props) { + constructor(props: Props) { super(props); this.state = { tab: 'palette', @@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component { }; } - setPickerNavElem(elem) { + setPickerNavElem(elem: any) { this.pickerNavElem = $(elem); } - setColor(color) { + setColor(color: string) { const newColor = tinycolor(color); if (newColor.isValid()) { - this.setState({ - color: newColor.toString(), - colorString: newColor.toString(), - }); + this.setState({ color: newColor.toString(), colorString: newColor.toString() }); this.props.onColorSelect(color); } } - sampleColorSelected(color) { + sampleColorSelected(color: string) { this.setColor(color); } - spectrumColorSelected(color) { + spectrumColorSelected(color: any) { const rgbColor = color.toRgbString(); this.setColor(rgbColor); } - onColorStringChange(e) { + onColorStringChange(e: any) { const colorString = e.target.value; - this.setState({ - colorString: colorString, - }); + this.setState({ colorString: colorString }); const newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state const newColorString = newColor.toString(); - this.setState({ - color: newColorString, - }); + this.setState({ color: newColorString }); this.props.onColorSelect(newColorString); } } - onColorStringBlur(e) { + onColorStringBlur(e: any) { const colorString = e.target.value; this.setColor(colorString); } componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', e => { + this.pickerNavElem.on('show', (e: any) => { // use href attr (#name => name) const tab = e.target.hash.slice(1); - this.setState({ - tab: tab, - }); + this.setState({ tab: tab }); }); } diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx similarity index 96% rename from public/app/core/components/colorpicker/SeriesColorPicker.tsx rename to packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx index 32b7554e38d..7c3848f6868 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx @@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component { onToggleAxis: () => {}, }; - constructor(props) { + constructor(props: SeriesColorPickerProps) { super(props); } @@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component { remove: true, tetherOptions: { constraints: [{ to: 'scrollParent', attachment: 'none both' }], + attachment: 'bottom center', }, }); diff --git a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx similarity index 85% rename from public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx rename to packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 085d554300d..541a77ddabc 100644 --- a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface SeriesColorPickerPopoverProps { color: string; @@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent void; + onToggleAxis?: () => void; } interface AxisSelectorState { @@ -30,7 +29,7 @@ interface AxisSelectorState { } export class AxisSelector extends React.PureComponent { - constructor(props) { + constructor(props: AxisSelectorProps) { super(props); this.state = { yaxis: this.props.yaxis, @@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent { elem: any; isMoving: boolean; - constructor(props) { + constructor(props: Props) { super(props); this.onSpectrumMove = this.onSpectrumMove.bind(this); this.setComponentElem = this.setComponentElem.bind(this); } - setComponentElem(elem) { + setComponentElem(elem: any) { this.elem = $(elem); } - onSpectrumMove(color) { + onSpectrumMove(color: any) { this.isMoving = true; this.props.onColorSelect(color); } @@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component { this.elem.spectrum('set', this.props.color); } - componentWillUpdate(nextProps) { + componentWillUpdate(nextProps: any) { // If user move pointer over spectrum field this produce 'move' event and component // may update props.color. We don't want to update spectrum color in this case, so we can use // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which diff --git a/public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap b/packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap similarity index 100% rename from public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap rename to packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx similarity index 100% rename from public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx rename to packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx similarity index 96% rename from public/app/core/components/CustomScrollbar/CustomScrollbar.tsx rename to packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 977892c637d..cf1657e1c83 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -12,7 +12,7 @@ interface Props { /** * Wraps component into component from `react-custom-scrollbars` */ -class CustomScrollbar extends PureComponent { +export class CustomScrollbar extends PureComponent { static defaultProps: Partial = { customClassName: 'custom-scrollbars', autoHide: true, diff --git a/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss new file mode 100644 index 00000000000..c0a8077fb63 --- /dev/null +++ b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss @@ -0,0 +1,40 @@ +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + flex-direction: column; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} \ No newline at end of file diff --git a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap similarity index 100% rename from public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap rename to packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap diff --git a/packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx similarity index 100% rename from packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx rename to packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx diff --git a/packages/grafana-ui/src/visualizations/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx similarity index 100% rename from packages/grafana-ui/src/visualizations/Graph/Graph.tsx rename to packages/grafana-ui/src/components/Graph/Graph.tsx diff --git a/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx new file mode 100644 index 00000000000..01048014f8a --- /dev/null +++ b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx @@ -0,0 +1,11 @@ +import React, { SFC } from 'react'; + +interface LoadingPlaceholderProps { + text: string; +} + +export const LoadingPlaceholder: SFC = ({ text }) => ( +
+ {text} +
+); diff --git a/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx b/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx new file mode 100644 index 00000000000..0636ec4a9da --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx @@ -0,0 +1,15 @@ +import React, { SFC } from 'react'; + +interface Props { + cols?: number; + children: JSX.Element[] | JSX.Element; +} + +export const PanelOptionsGrid: SFC = ({ children }) => { + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss b/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss new file mode 100644 index 00000000000..1cd26867a97 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss @@ -0,0 +1,10 @@ +.panel-options-grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + grid-row-gap: 10px; + grid-column-gap: 10px; + + @include media-breakpoint-up(lg) { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelOptionSection.tsx b/packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx similarity index 65% rename from public/app/features/dashboard/dashgrid/PanelOptionSection.tsx rename to packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx index f38d99d2237..7ce4b8335ff 100644 --- a/public/app/features/dashboard/dashgrid/PanelOptionSection.tsx +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx @@ -7,11 +7,11 @@ interface Props { children: JSX.Element | JSX.Element[]; } -export const PanelOptionSection: SFC = props => { +export const PanelOptionsGroup: SFC = props => { return ( -
+
{props.title && ( -
+
{props.title} {props.onClose && (
)} -
{props.children}
+
{props.children}
); }; diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss new file mode 100644 index 00000000000..9f5d4f02695 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -0,0 +1,27 @@ +.panel-options-group { + margin-bottom: 10px; + border: $panel-options-group-border; + border-radius: $border-radius; + background: $page-bg; +} + +.panel-options-group__header { + padding: 4px 20px; + font-size: 1.1rem; + background: $panel-options-group-header-bg; + position: relative; + + .btn { + position: absolute; + right: 0; + top: 0px; + } +} + +.panel-options-group__body { + padding: 20px; + + &--queries { + min-height: 200px; + } +} diff --git a/public/app/core/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx similarity index 87% rename from public/app/core/components/Portal/Portal.tsx rename to packages/grafana-ui/src/components/Portal/Portal.tsx index 25d54a64209..6f51f4053e2 100644 --- a/public/app/core/components/Portal/Portal.tsx +++ b/packages/grafana-ui/src/components/Portal/Portal.tsx @@ -6,11 +6,11 @@ interface Props { root?: HTMLElement; } -export default class BodyPortal extends PureComponent { +export class Portal extends PureComponent { node: HTMLElement = document.createElement('div'); portalRoot: HTMLElement; - constructor(props) { + constructor(props: Props) { super(props); const { className, diff --git a/public/app/core/components/Select/IndicatorsContainer.tsx b/packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx similarity index 73% rename from public/app/core/components/Select/IndicatorsContainer.tsx rename to packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx index d4de51a2cef..260fe6ebbdf 100644 --- a/public/app/core/components/Select/IndicatorsContainer.tsx +++ b/packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx @@ -1,7 +1,10 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; -export const IndicatorsContainer = props => { +export const IndicatorsContainer = (props: any) => { const isOpen = props.selectProps.menuIsOpen; return ( diff --git a/public/app/core/components/Select/NoOptionsMessage.tsx b/packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx similarity index 83% rename from public/app/core/components/Select/NoOptionsMessage.tsx rename to packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx index 5fe229340a4..1cec06a5301 100644 --- a/public/app/core/components/Select/NoOptionsMessage.tsx +++ b/packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx @@ -1,5 +1,9 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; +// @ts-ignore import { OptionProps } from '@torkelo/react-select/lib/components/Option'; export interface Props { diff --git a/public/app/core/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx similarity index 91% rename from public/app/core/components/Select/Select.tsx rename to packages/grafana-ui/src/components/Select/Select.tsx index 893eb1a6655..b3b0c8efbbb 100644 --- a/public/app/core/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -1,17 +1,22 @@ // Libraries import classNames from 'classnames'; import React, { PureComponent } from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { default as ReactSelect } from '@torkelo/react-select'; +// @ts-ignore import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async'; +// @ts-ignore import { components } from '@torkelo/react-select'; // Components -import { Option, SingleValue } from './PickerOption'; -import OptionGroup from './OptionGroup'; +import { SelectOption, SingleValue } from './SelectOption'; +import SelectOptionGroup from './SelectOptionGroup'; import IndicatorsContainer from './IndicatorsContainer'; import NoOptionsMessage from './NoOptionsMessage'; -import ResetStyles from './ResetStyles'; -import CustomScrollbar from '../CustomScrollbar/CustomScrollbar'; +import resetSelectStyles from './resetSelectStyles'; +import { CustomScrollbar } from '@grafana/ui'; export interface SelectOptionItem { label?: string; @@ -53,7 +58,7 @@ interface AsyncProps { loadingMessage?: () => string; } -export const MenuList = props => { +export const MenuList = (props: any) => { return ( {props.children} @@ -112,11 +117,11 @@ export class Select extends PureComponent { classNamePrefix="gf-form-select-box" className={selectClassNames} components={{ - Option, + Option: SelectOption, SingleValue, IndicatorsContainer, MenuList, - Group: OptionGroup, + Group: SelectOptionGroup, }} defaultValue={defaultValue} value={value} @@ -127,7 +132,7 @@ export class Select extends PureComponent { onChange={onChange} options={options} placeholder={placeholder || 'Choose'} - styles={ResetStyles} + styles={resetSelectStyles()} isDisabled={isDisabled} isLoading={isLoading} isClearable={isClearable} @@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent { isLoading={isLoading} defaultOptions={defaultOptions} placeholder={placeholder || 'Choose'} - styles={ResetStyles} + styles={resetSelectStyles()} loadingMessage={loadingMessage} noOptionsMessage={noOptionsMessage} isDisabled={isDisabled} diff --git a/public/app/core/components/Select/PickerOption.test.tsx b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx similarity index 51% rename from public/app/core/components/Select/PickerOption.test.tsx rename to packages/grafana-ui/src/components/Select/SelectOption.test.tsx index 6b4aedcfcc0..778da9b82b5 100644 --- a/public/app/core/components/Select/PickerOption.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import PickerOption from './PickerOption'; +import SelectOption from './SelectOption'; +import { OptionProps } from 'react-select/lib/components/Option'; -const model = { +// @ts-ignore +const model: OptionProps = { cx: jest.fn(), clearValue: jest.fn(), - onSelect: jest.fn(), getStyles: jest.fn(), getValue: jest.fn(), hasValue: true, @@ -18,21 +19,31 @@ const model = { isFocused: false, isSelected: false, innerRef: null, - innerProps: null, - label: 'Option label', - type: null, - children: 'Model title', - data: { - title: 'Model title', - imgUrl: 'url/to/avatar', - label: 'User picker label', + innerProps: { + id: '', + key: '', + onClick: jest.fn(), + onMouseOver: jest.fn(), + tabIndex: 1, }, + label: 'Option label', + type: 'option', + children: 'Model title', className: 'class-for-user-picker', }; -describe('PickerOption', () => { +describe('SelectOption', () => { it('renders correctly', () => { - const tree = renderer.create().toJSON(); + const tree = renderer + .create( + + ) + .toJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/Select/PickerOption.tsx b/packages/grafana-ui/src/components/Select/SelectOption.tsx similarity index 84% rename from public/app/core/components/Select/PickerOption.tsx rename to packages/grafana-ui/src/components/Select/SelectOption.tsx index d263f6f832b..5f94c2182f9 100644 --- a/public/app/core/components/Select/PickerOption.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.tsx @@ -1,4 +1,7 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; import { OptionProps } from 'react-select/lib/components/Option'; @@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps { }; } -export const Option = (props: ExtendedOptionProps) => { +export const SelectOption = (props: ExtendedOptionProps) => { const { children, isSelected, data } = props; return ( @@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => { }; // was not able to type this without typescript error -export const SingleValue = props => { +export const SingleValue = (props: any) => { const { children, data } = props; return ( @@ -41,4 +44,4 @@ export const SingleValue = props => { ); }; -export default Option; +export default SelectOption; diff --git a/public/app/core/components/Select/OptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx similarity index 89% rename from public/app/core/components/Select/OptionGroup.tsx rename to packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index a001f58c681..30842f02e29 100644 --- a/public/app/core/components/Select/OptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -9,7 +9,7 @@ interface State { expanded: boolean; } -export default class OptionGroup extends PureComponent { +export default class SelectOptionGroup extends PureComponent { state = { expanded: false, }; @@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent +exports[`SelectOption renders correctly 1`] = ` +
diff --git a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts new file mode 100644 index 00000000000..a980741c17c --- /dev/null +++ b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts @@ -0,0 +1,27 @@ +export default function resetSelectStyles() { + return { + clearIndicator: () => ({}), + container: () => ({}), + control: () => ({}), + dropdownIndicator: () => ({}), + group: () => ({}), + groupHeading: () => ({}), + indicatorsContainer: () => ({}), + indicatorSeparator: () => ({}), + input: () => ({}), + loadingIndicator: () => ({}), + loadingMessage: () => ({}), + menu: () => ({}), + menuList: ({ maxHeight }: { maxHeight: number }) => ({ + maxHeight, + }), + multiValue: () => ({}), + multiValueLabel: () => ({}), + multiValueRemove: () => ({}), + noOptionsMessage: () => ({}), + option: () => ({}), + placeholder: () => ({}), + singleValue: () => ({}), + valueContainer: () => ({}), + }; +} diff --git a/public/app/plugins/panel/gauge/Threshold.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx similarity index 73% rename from public/app/plugins/panel/gauge/Threshold.test.tsx rename to packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 3fa508b98a9..14f84e00f80 100644 --- a/public/app/plugins/panel/gauge/Threshold.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,22 +1,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import Thresholds from './Thresholds'; -import { defaultProps, OptionsProps } from './module'; -import { BasicGaugeColor } from 'app/types'; -import { PanelOptionsProps } from '@grafana/ui'; + +import { ThresholdsEditor, Props } from './ThresholdsEditor'; +import { BasicGaugeColor } from '../../types'; const setup = (propOverrides?: object) => { - const props: PanelOptionsProps = { + const props: Props = { onChange: jest.fn(), - options: { - ...defaultProps.options, - thresholds: [], - }, + thresholds: [], }; Object.assign(props, propOverrides); - return shallow().instance() as Thresholds; + return shallow().instance() as ThresholdsEditor; }; describe('Add threshold', () => { @@ -30,10 +26,7 @@ describe('Add threshold', () => { it('should add another threshold above a first', () => { const instance = setup({ - options: { - ...defaultProps.options, - thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }], - }, + thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }], }); instance.onAddThreshold(1); diff --git a/public/app/plugins/panel/gauge/Thresholds.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx similarity index 66% rename from public/app/plugins/panel/gauge/Thresholds.tsx rename to packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index dd0dcc1e33b..c635b9cb4f5 100644 --- a/public/app/plugins/panel/gauge/Thresholds.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,31 +1,38 @@ import React, { PureComponent } from 'react'; -import tinycolor from 'tinycolor2'; -import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker'; -import { OptionModuleProps } from './module'; -import { BasicGaugeColor, Threshold } from 'app/types'; +import tinycolor, { ColorInput } from 'tinycolor2'; + +import { Threshold, BasicGaugeColor } from '../../types'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; +import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; + +export interface Props { + thresholds: Threshold[]; + onChange: (thresholds: Threshold[]) => void; +} interface State { thresholds: Threshold[]; baseColor: string; } -export default class Thresholds extends PureComponent { - constructor(props) { +export class ThresholdsEditor extends PureComponent { + constructor(props: Props) { super(props); - this.state = { - thresholds: props.options.thresholds, - baseColor: props.options.baseColor, - }; + this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green }; } - onAddThreshold = index => { - const { maxValue, minValue } = this.props.options; + onAddThreshold = (index: number) => { + const maxValue = 100; // hardcoded for now before we add the base threshold + const minValue = 0; // hardcoded for now before we add the base threshold const { thresholds } = this.state; const newThresholds = thresholds.map(threshold => { if (threshold.index >= index) { - threshold = { ...threshold, index: threshold.index + 1 }; + threshold = { + ...threshold, + index: threshold.index + 1, + }; } return threshold; @@ -47,27 +54,32 @@ export default class Thresholds extends PureComponent if (index === 0 && thresholds.length === 0) { color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString(); } else { - color = tinycolor.mix(thresholds[index - 1].color, BasicGaugeColor.Red, 50).toRgbString(); + color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString(); } this.setState( { - thresholds: this.sortThresholds([...newThresholds, { index: index, value: value, color: color }]), + thresholds: this.sortThresholds([ + ...newThresholds, + { + index, + value: value as number, + color, + }, + ]), }, () => this.updateGauge() ); }; - onRemoveThreshold = threshold => { + onRemoveThreshold = (threshold: Threshold) => { this.setState( - prevState => ({ - thresholds: prevState.thresholds.filter(t => t !== threshold), - }), + prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }), () => this.updateGauge() ); }; - onChangeThresholdValue = (event, threshold) => { + onChangeThresholdValue = (event: any, threshold: Threshold) => { const { thresholds } = this.state; const newThresholds = thresholds.map(t => { @@ -78,12 +90,10 @@ export default class Thresholds extends PureComponent return t; }); - this.setState({ - thresholds: newThresholds, - }); + this.setState({ thresholds: newThresholds }); }; - onChangeThresholdColor = (threshold, color) => { + onChangeThresholdColor = (threshold: Threshold, color: string) => { const { thresholds } = this.state; const newThresholds = thresholds.map(t => { @@ -102,20 +112,18 @@ export default class Thresholds extends PureComponent ); }; - onChangeBaseColor = color => this.props.onChange({ ...this.props.options, baseColor: color }); + onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds); onBlur = () => { - this.setState(prevState => ({ - thresholds: this.sortThresholds(prevState.thresholds), - })); + this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) })); this.updateGauge(); }; updateGauge = () => { - this.props.onChange({ ...this.props.options, thresholds: this.state.thresholds }); + this.props.onChange(this.state.thresholds); }; - sortThresholds = thresholds => { + sortThresholds = (thresholds: Threshold[]) => { return thresholds.sort((t1, t2) => { return t2.value - t1.value; }); @@ -160,20 +168,8 @@ export default class Thresholds extends PureComponent return thresholds.map((t, i) => { return (
-
this.onAddThreshold(t.index + 1)} - style={{ - height: '50%', - backgroundColor: t.color, - }} - /> -
this.onAddThreshold(t.index)} - style={{ - height: '50%', - backgroundColor: t.color, - }} - /> +
this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} /> +
this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
); }); @@ -184,14 +180,14 @@ export default class Thresholds extends PureComponent
this.onAddThreshold(0)} - style={{ height: '100%', backgroundColor: this.props.options.baseColor }} + style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }} />
); } renderBase() { - const { baseColor } = this.props.options; + const baseColor = BasicGaugeColor.Green; return (
@@ -209,8 +205,7 @@ export default class Thresholds extends PureComponent render() { return ( -
-
Thresholds
+
{this.renderIndicator()} @@ -221,7 +216,7 @@ export default class Thresholds extends PureComponent {this.renderBase()}
-
+ ); } } diff --git a/public/sass/components/_thresholds.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss similarity index 100% rename from public/sass/components/_thresholds.scss rename to packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss diff --git a/public/app/core/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx similarity index 73% rename from public/app/core/components/Tooltip/Popper.tsx rename to packages/grafana-ui/src/components/Tooltip/Popper.tsx index 65ef510ba8f..eb64df1cb6e 100644 --- a/public/app/core/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -1,11 +1,13 @@ import React, { PureComponent } from 'react'; -import Portal from 'app/core/components/Portal/Portal'; -import { Manager, Popper as ReactPopper, Reference } from 'react-popper'; +import * as PopperJS from 'popper.js'; +import { Manager, Popper as ReactPopper } from 'react-popper'; +import { Portal } from '@grafana/ui'; import Transition from 'react-transition-group/Transition'; export enum Themes { Default = 'popper__background--default', Error = 'popper__background--error', + Brand = 'popper__background--brand', } const defaultTransitionStyles = { @@ -13,45 +15,40 @@ const defaultTransitionStyles = { opacity: 0, }; -const transitionStyles = { +const transitionStyles: {[key: string]: object} = { exited: { opacity: 0 }, entering: { opacity: 0 }, entered: { opacity: 1 }, exiting: { opacity: 0 }, }; -interface Props { +interface Props extends React.DOMAttributes { renderContent: (content: any) => any; show: boolean; - placement?: any; + placement?: PopperJS.Placement; content: string | ((props: any) => JSX.Element); - refClassName?: string; + referenceElement: PopperJS.ReferenceObject; theme?: Themes; } class Popper extends PureComponent { render() { - const { children, renderContent, show, placement, refClassName, theme } = this.props; + const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props; const { content } = this.props; const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : ''); return ( - - {({ ref }) => ( -
- {children} -
- )} -
{transitionState => ( - + {({ ref, style, placement, arrowProps }) => { return (
JSX.Element); + +export interface UsingPopperProps { + show?: boolean; + placement?: PopperJS.Placement; + content: PopperContent; + children: JSX.Element; + renderContent?: (content: PopperContent) => JSX.Element; + theme?: Themes; +} + +type PopperControllerRenderProp = ( + showPopper: () => void, + hidePopper: () => void, + popperProps: { + show: boolean; + placement: PopperJS.Placement; + content: string | ((props: any) => JSX.Element); + renderContent: (content: any) => any; + theme?: Themes; + } +) => JSX.Element; + +interface Props { + placement?: PopperJS.Placement; + content: PopperContent; + className?: string; + children: PopperControllerRenderProp; + theme?: Themes; +} + +interface State { + placement: PopperJS.Placement; + show: boolean; +} + +class PopperController extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + placement: this.props.placement || 'auto', + show: false, + }; + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.placement && nextProps.placement !== this.state.placement) { + this.setState((prevState: State) => { + return { + ...prevState, + placement: nextProps.placement || 'auto', + }; + }); + } + } + + showPopper = () => { + this.setState(prevState => ({ + ...prevState, + show: true, + })); + }; + + hidePopper = () => { + this.setState(prevState => ({ + ...prevState, + show: false, + })); + }; + + renderContent(content: PopperContent) { + if (typeof content === 'function') { + // If it's a function we assume it's a React component + const ReactComponent = content; + return ; + } + return content; + } + + render() { + const { children, content, theme } = this.props; + const { show, placement } = this.state; + + return children(this.showPopper, this.hidePopper, { + show, + placement, + content, + renderContent: this.renderContent, + theme, + }); + } +} + +export default PopperController; diff --git a/public/app/core/components/Tooltip/Tooltip.test.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx similarity index 56% rename from public/app/core/components/Tooltip/Tooltip.test.tsx rename to packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx index d2c96bb23d2..95d01c7f2fe 100644 --- a/public/app/core/components/Tooltip/Tooltip.test.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx @@ -1,13 +1,15 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import Tooltip from './Tooltip'; +import { Tooltip } from './Tooltip'; describe('Tooltip', () => { it('renders correctly', () => { const tree = renderer .create( - - Link with tooltip + + + Link with tooltip + ) .toJSON(); diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 00000000000..9cffb151d83 --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,32 @@ +import React, { createRef } from 'react'; +import * as PopperJS from 'popper.js'; +import Popper from './Popper'; +import PopperController, { UsingPopperProps } from './PopperController'; + +export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => { + const tooltipTriggerRef = createRef(); + + return ( + + {(showPopper, hidePopper, popperProps) => { + return ( + <> + {tooltipTriggerRef.current && ( + + )} + {React.cloneElement(children, { + ref: tooltipTriggerRef, + onMouseEnter: showPopper, + onMouseLeave: hidePopper, + })} + + ); + }} + + ); +}; diff --git a/public/sass/components/_popper.scss b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss similarity index 86% rename from public/sass/components/_popper.scss rename to packages/grafana-ui/src/components/Tooltip/_Tooltip.scss index afa629d4043..c8fa099cce6 100644 --- a/public/sass/components/_popper.scss +++ b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss @@ -1,5 +1,13 @@ $popper-margin-from-ref: 5px; + +@mixin popper-theme($backgroundColor, $arrowColor) { + background: $backgroundColor; + .popper__arrow { + border-color: $arrowColor; + } +} + .popper { position: absolute; z-index: $zindex-tooltip; @@ -16,10 +24,12 @@ $popper-margin-from-ref: 5px; // Themes &.popper__background--error { - background: $tooltipBackgroundError; - .popper__arrow { - border-color: $tooltipBackgroundError; - } + @include popper-theme($tooltipBackgroundError, $tooltipBackgroundError); + } + + &.popper__background--brand { + @include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand); + @include gradient-vertical($red, $orange); } } diff --git a/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap b/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap new file mode 100644 index 00000000000..761221906d4 --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tooltip renders correctly 1`] = ` + + Link with tooltip + +`; diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index d52508c946c..5a9263844a4 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1 +1,7 @@ +@import 'CustomScrollbar/CustomScrollbar'; @import 'DeleteButton/DeleteButton'; +@import 'ThresholdsEditor/ThresholdsEditor'; +@import 'Tooltip/Tooltip'; +@import 'Select/Select'; +@import 'PanelOptionsGroup/PanelOptionsGroup'; +@import 'PanelOptionsGrid/PanelOptionsGrid'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index b57b9bcfdb7..5420fcf14b7 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -1 +1,20 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; +export { Tooltip } from './Tooltip/Tooltip'; +export { Portal } from './Portal/Portal'; +export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; + +// Select +export { Select, AsyncSelect, SelectOptionItem } from './Select/Select'; +export { IndicatorsContainer } from './Select/IndicatorsContainer'; +export { NoOptionsMessage } from './Select/NoOptionsMessage'; +export { default as resetSelectStyles } from './Select/resetSelectStyles'; + +export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; +export { ColorPicker } from './ColorPicker/ColorPicker'; +export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; +export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; +export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; +export { GfFormLabel } from './GfFormLabel/GfFormLabel'; +export { Graph } from './Graph/Graph'; +export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; +export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; diff --git a/packages/grafana-ui/src/forms/index.ts b/packages/grafana-ui/src/forms/index.ts deleted file mode 100644 index bb6998b0025..00000000000 --- a/packages/grafana-ui/src/forms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index b22152497b9..974d976bbef 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -1,5 +1,3 @@ export * from './components'; -export * from './visualizations'; export * from './types'; export * from './utils'; -export * from './forms'; diff --git a/packages/grafana-ui/src/types/gauge.ts b/packages/grafana-ui/src/types/gauge.ts new file mode 100644 index 00000000000..e05849448f7 --- /dev/null +++ b/packages/grafana-ui/src/types/gauge.ts @@ -0,0 +1,16 @@ +import { RangeMap, Threshold, ValueMap } from './panel'; + +export interface GaugeOptions { + baseColor: string; + decimals: number; + mappings: Array; + maxValue: number; + minValue: number; + prefix: string; + showThresholdLabels: boolean; + showThresholdMarkers: boolean; + stat: string; + suffix: string; + thresholds: Threshold[]; + unit: string; +} diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index f618ce6db34..814ab0478db 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -1,3 +1,4 @@ export * from './series'; export * from './time'; export * from './panel'; +export * from './gauge'; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 44336555a81..0b995f932f0 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -29,3 +29,35 @@ export interface PanelMenuItem { shortcut?: string; subMenu?: PanelMenuItem[]; } + +export interface Threshold { + index: number; + value: number; + color?: string; +} + +export enum BasicGaugeColor { + Green = '#299c46', + Red = '#d44a3a', +} + +export enum MappingType { + ValueToText = 1, + RangeToText = 2, +} + +interface BaseMap { + id: number; + operator: string; + text: string; + type: MappingType; +} + +export interface ValueMap extends BaseMap { + value: string; +} + +export interface RangeMap extends BaseMap { + from: string; + to: string; +} diff --git a/packages/grafana-ui/src/utils/colors.ts b/packages/grafana-ui/src/utils/colors.ts new file mode 100644 index 00000000000..263d128aec4 --- /dev/null +++ b/packages/grafana-ui/src/utils/colors.ts @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import tinycolor from 'tinycolor2'; + +export const PALETTE_ROWS = 4; +export const PALETTE_COLUMNS = 14; +export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; +export const OK_COLOR = 'rgba(11, 237, 50, 1)'; +export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; +export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; +export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; +export const REGION_FILL_ALPHA = 0.09; + +export const colors = [ + '#7EB26D', // 0: pale green + '#EAB839', // 1: mustard + '#6ED0E0', // 2: light blue + '#EF843C', // 3: orange + '#E24D42', // 4: red + '#1F78C1', // 5: ocean + '#BA43A9', // 6: purple + '#705DA0', // 7: violet + '#508642', // 8: dark green + '#CCA300', // 9: dark sand + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', +]; + +function sortColorsByHue(hexColors: string[]) { + const hslColors = _.map(hexColors, hexToHsl); + + const sortedHSLColors = _.sortBy(hslColors, ['h']); + const chunkedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); + const sortedChunkedHSLColors = _.map(chunkedHSLColors, chunk => { + return _.sortBy(chunk, 'l'); + }); + const flattenedZippedSortedChunkedHSLColors = _.flattenDeep(_.zip(...sortedChunkedHSLColors)); + + return _.map(flattenedZippedSortedChunkedHSLColors, hslToHex); +} + +function hexToHsl(color: string) { + return tinycolor(color).toHsl(); +} + +function hslToHex(color: any) { + return tinycolor(color).toHexString(); +} + +export let sortedColors = sortColorsByHue(colors); diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 4d9b9a4b948..aeb65032067 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -1 +1,3 @@ export * from './processTimeSeries'; +export * from './valueFormats/valueFormats'; +export * from './colors'; diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts new file mode 100644 index 00000000000..44332a51307 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts @@ -0,0 +1,40 @@ +import { toHex, toHex0x } from './arithmeticFormatters'; + +describe('hex', () => { + it('positive integer', () => { + const str = toHex(100, 0); + expect(str).toBe('64'); + }); + it('negative integer', () => { + const str = toHex(-100, 0); + expect(str).toBe('-64'); + }); + it('positive float', () => { + const str = toHex(50.52, 1); + expect(str).toBe('32.8'); + }); + it('negative float', () => { + const str = toHex(-50.333, 2); + expect(str).toBe('-32.547AE147AE14'); + }); +}); + +describe('hex 0x', () => { + it('positive integeter', () => { + const str = toHex0x(7999, 0); + expect(str).toBe('0x1F3F'); + }); + it('negative integer', () => { + const str = toHex0x(-584, 0); + expect(str).toBe('-0x248'); + }); + + it('positive float', () => { + const str = toHex0x(74.443, 3); + expect(str).toBe('0x4A.716872B020C4'); + }); + it('negative float', () => { + const str = toHex0x(-65.458, 1); + expect(str).toBe('-0x41.8'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts new file mode 100644 index 00000000000..fa9daf0fb97 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts @@ -0,0 +1,42 @@ +import { toFixed } from './valueFormats'; + +export function toPercent(size: number, decimals: number) { + if (size === null) { + return ''; + } + return toFixed(size, decimals) + '%'; +} + +export function toPercentUnit(size: number, decimals: number) { + if (size === null) { + return ''; + } + return toFixed(100 * size, decimals) + '%'; +} + +export function toHex0x(value: number, decimals: number) { + if (value == null) { + return ''; + } + const hexString = toHex(value, decimals); + if (hexString.substring(0, 1) === '-') { + return '-0x' + hexString.substring(1); + } + return '0x' + hexString; +} + +export function toHex(value: number, decimals: number) { + if (value == null) { + return ''; + } + return parseFloat(toFixed(value, decimals)) + .toString(16) + .toUpperCase(); +} + +export function sci(value: number, decimals: number) { + if (value == null) { + return ''; + } + return value.toExponential(decimals); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/categories.ts b/packages/grafana-ui/src/utils/valueFormats/categories.ts new file mode 100644 index 00000000000..d7410c22276 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/categories.ts @@ -0,0 +1,322 @@ +import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats'; +import { + dateTimeAsIso, + dateTimeAsUS, + dateTimeFromNow, + toClockMilliseconds, + toClockSeconds, + toDays, + toDurationInHoursMinutesSeconds, + toDurationInMilliseconds, + toDurationInSeconds, + toHours, + toMicroSeconds, + toMilliSeconds, + toMinutes, + toNanoSeconds, + toSeconds, + toTimeTicks, +} from './dateTimeFormatters'; +import { toHex, sci, toHex0x, toPercent, toPercentUnit } from './arithmeticFormatters'; +import { binarySIPrefix, currency, decimalSIPrefix } from './symbolFormatters'; + +export const getCategories = (): ValueFormatCategory[] => [ + { + name: 'Misc', + formats: [ + { name: 'none', id: 'none', fn: toFixed }, + { + name: 'short', + id: 'short', + fn: scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']), + }, + { name: 'percent (0-100)', id: 'percent', fn: toPercent }, + { name: 'percent (0.0-1.0)', id: 'percentunit', fn: toPercentUnit }, + { name: 'Humidity (%H)', id: 'humidity', fn: toFixedUnit('%H') }, + { name: 'decibel', id: 'dB', fn: toFixedUnit('dB') }, + { name: 'hexadecimal (0x)', id: 'hex0x', fn: toHex0x }, + { name: 'hexadecimal', id: 'hex', fn: toHex }, + { name: 'scientific notation', id: 'sci', fn: sci }, + { name: 'locale format', id: 'locale', fn: locale }, + ], + }, + { + name: 'Acceleration', + formats: [ + { name: 'Meters/sec²', id: 'accMS2', fn: toFixedUnit('m/sec²') }, + { name: 'Feet/sec²', id: 'accFS2', fn: toFixedUnit('f/sec²') }, + { name: 'G unit', id: 'accG', fn: toFixedUnit('g') }, + ], + }, + { + name: 'Angle', + formats: [ + { name: 'Degrees (°)', id: 'degree', fn: toFixedUnit('°') }, + { name: 'Radians', id: 'radian', fn: toFixedUnit('rad') }, + { name: 'Gradian', id: 'grad', fn: toFixedUnit('grad') }, + ], + }, + { + name: 'Area', + formats: [ + { name: 'Square Meters (m²)', id: 'areaM2', fn: toFixedUnit('m²') }, + { name: 'Square Feet (ft²)', id: 'areaF2', fn: toFixedUnit('ft²') }, + { name: 'Square Miles (mi²)', id: 'areaMI2', fn: toFixedUnit('mi²') }, + ], + }, + { + name: 'Computation', + formats: [ + { name: 'FLOP/s', id: 'flops', fn: decimalSIPrefix('FLOP/s') }, + { name: 'MFLOP/s', id: 'mflops', fn: decimalSIPrefix('FLOP/s', 2) }, + { name: 'GFLOP/s', id: 'gflops', fn: decimalSIPrefix('FLOP/s', 3) }, + { name: 'TFLOP/s', id: 'tflops', fn: decimalSIPrefix('FLOP/s', 4) }, + { name: 'PFLOP/s', id: 'pflops', fn: decimalSIPrefix('FLOP/s', 5) }, + { name: 'EFLOP/s', id: 'eflops', fn: decimalSIPrefix('FLOP/s', 6) }, + ], + }, + { + name: 'Concentration', + formats: [ + { name: 'parts-per-million (ppm)', id: 'ppm', fn: toFixedUnit('ppm') }, + { name: 'parts-per-billion (ppb)', id: 'conppb', fn: toFixedUnit('ppb') }, + { name: 'nanogram per cubic meter (ng/m³)', id: 'conngm3', fn: toFixedUnit('ng/m³') }, + { name: 'nanogram per normal cubic meter (ng/Nm³)', id: 'conngNm3', fn: toFixedUnit('ng/Nm³') }, + { name: 'microgram per cubic meter (μg/m³)', id: 'conμgm3', fn: toFixedUnit('μg/m³') }, + { name: 'microgram per normal cubic meter (μg/Nm³)', id: 'conμgNm3', fn: toFixedUnit('μg/Nm³') }, + { name: 'milligram per cubic meter (mg/m³)', id: 'conmgm3', fn: toFixedUnit('mg/m³') }, + { name: 'milligram per normal cubic meter (mg/Nm³)', id: 'conmgNm3', fn: toFixedUnit('mg/Nm³') }, + { name: 'gram per cubic meter (g/m³)', id: 'congm3', fn: toFixedUnit('g/m³') }, + { name: 'gram per normal cubic meter (g/Nm³)', id: 'congNm3', fn: toFixedUnit('g/Nm³') }, + { name: 'milligrams per decilitre (mg/dL)', id: 'conmgdL', fn: toFixedUnit('mg/dL') }, + { name: 'millimoles per litre (mmol/L)', id: 'conmmolL', fn: toFixedUnit('mmol/L') }, + ], + }, + { + name: 'Currency', + formats: [ + { name: 'Dollars ($)', id: 'currencyUSD', fn: currency('$') }, + { name: 'Pounds (£)', id: 'currencyGBP', fn: currency('£') }, + { name: 'Euro (€)', id: 'currencyEUR', fn: currency('€') }, + { name: 'Yen (¥)', id: 'currencyJPY', fn: currency('¥') }, + { name: 'Rubles (₽)', id: 'currencyRUB', fn: currency('₽') }, + { name: 'Hryvnias (₴)', id: 'currencyUAH', fn: currency('₴') }, + { name: 'Real (R$)', id: 'currencyBRL', fn: currency('R$') }, + { name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr') }, + { name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr') }, + { name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr') }, + { name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr') }, + { name: 'Czech koruna (czk)', id: 'currencyCZK', fn: currency('czk') }, + { name: 'Swiss franc (CHF)', id: 'currencyCHF', fn: currency('CHF') }, + { name: 'Polish Złoty (PLN)', id: 'currencyPLN', fn: currency('PLN') }, + { name: 'Bitcoin (฿)', id: 'currencyBTC', fn: currency('฿') }, + ], + }, + { + name: 'Data (IEC)', + formats: [ + { name: 'bits', id: 'bits', fn: binarySIPrefix('b') }, + { name: 'bytes', id: 'bytes', fn: binarySIPrefix('B') }, + { name: 'kibibytes', id: 'kbytes', fn: binarySIPrefix('B', 1) }, + { name: 'mebibytes', id: 'mbytes', fn: binarySIPrefix('B', 2) }, + { name: 'gibibytes', id: 'gbytes', fn: binarySIPrefix('B', 3) }, + ], + }, + { + name: 'Data (Metric)', + formats: [ + { name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') }, + { name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') }, + { name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) }, + { name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) }, + { name: 'gigabytes', id: 'decgbytes', fn: decimalSIPrefix('B', 3) }, + ], + }, + { + name: 'Data Rate', + formats: [ + { name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') }, + { name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') }, + { name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') }, + { name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) }, + { name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) }, + { name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) }, + { name: 'megabits/sec', id: 'Mbits', fn: decimalSIPrefix('bps', 2) }, + { name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('Bs', 3) }, + { name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) }, + ], + }, + { + name: 'Date & Time', + formats: [ + { name: 'YYYY-MM-DD HH:mm:ss', id: 'dateTimeAsIso', fn: dateTimeAsIso }, + { name: 'DD/MM/YYYY h:mm:ss a', id: 'dateTimeAsUS', fn: dateTimeAsUS }, + { name: 'From Now', id: 'dateTimeFromNow', fn: dateTimeFromNow }, + ], + }, + { + name: 'Energy', + formats: [ + { name: 'Watt (W)', id: 'watt', fn: decimalSIPrefix('W') }, + { name: 'Kilowatt (kW)', id: 'kwatt', fn: decimalSIPrefix('W', 1) }, + { name: 'Milliwatt (mW)', id: 'mwatt', fn: decimalSIPrefix('W', -1) }, + { name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') }, + { name: 'Volt-ampere (VA)', id: 'voltamp', fn: decimalSIPrefix('VA') }, + { name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: decimalSIPrefix('VA', 1) }, + { name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: decimalSIPrefix('var') }, + { name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: decimalSIPrefix('var', 1) }, + { name: 'Watt-hour (Wh)', id: 'watth', fn: decimalSIPrefix('Wh') }, + { name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: decimalSIPrefix('Wh', 1) }, + { name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: decimalSIPrefix('W/Min', 1) }, + { name: 'Joule (J)', id: 'joule', fn: decimalSIPrefix('J') }, + { name: 'Electron volt (eV)', id: 'ev', fn: decimalSIPrefix('eV') }, + { name: 'Ampere (A)', id: 'amp', fn: decimalSIPrefix('A') }, + { name: 'Kiloampere (kA)', id: 'kamp', fn: decimalSIPrefix('A', 1) }, + { name: 'Milliampere (mA)', id: 'mamp', fn: decimalSIPrefix('A', -1) }, + { name: 'Volt (V)', id: 'volt', fn: decimalSIPrefix('V') }, + { name: 'Kilovolt (kV)', id: 'kvolt', fn: decimalSIPrefix('V', 1) }, + { name: 'Millivolt (mV)', id: 'mvolt', fn: decimalSIPrefix('V', -1) }, + { name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: decimalSIPrefix('dBm') }, + { name: 'Ohm (Ω)', id: 'ohm', fn: decimalSIPrefix('Ω') }, + { name: 'Lumens (Lm)', id: 'lumens', fn: decimalSIPrefix('Lm') }, + ], + }, + { + name: 'Flow', + formats: [ + { name: 'Gallons/min (gpm)', id: 'flowgpm', fn: toFixedUnit('gpm') }, + { name: 'Cubic meters/sec (cms)', id: 'flowcms', fn: toFixedUnit('cms') }, + { name: 'Cubic feet/sec (cfs)', id: 'flowcfs', fn: toFixedUnit('cfs') }, + { name: 'Cubic feet/min (cfm)', id: 'flowcfm', fn: toFixedUnit('cfm') }, + { name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('l/h') }, + { name: 'Litre/min (l/min)', id: 'flowlpm', fn: toFixedUnit('l/min') }, + { name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') }, + ], + }, + { + name: 'Force', + formats: [ + { name: 'Newton-meters (Nm)', id: 'forceNm', fn: decimalSIPrefix('Nm') }, + { name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: decimalSIPrefix('Nm', 1) }, + { name: 'Newtons (N)', id: 'forceN', fn: decimalSIPrefix('N') }, + { name: 'Kilonewtons (kN)', id: 'forcekN', fn: decimalSIPrefix('N', 1) }, + ], + }, + { + name: 'Hash Rate', + formats: [ + { name: 'hashes/sec', id: 'Hs', fn: decimalSIPrefix('H/s') }, + { name: 'kilohashes/sec', id: 'KHs', fn: decimalSIPrefix('H/s', 1) }, + { name: 'megahashes/sec', id: 'MHs', fn: decimalSIPrefix('H/s', 2) }, + { name: 'gigahashes/sec', id: 'GHs', fn: decimalSIPrefix('H/s', 3) }, + { name: 'terahashes/sec', id: 'THs', fn: decimalSIPrefix('H/s', 4) }, + { name: 'petahashes/sec', id: 'PHs', fn: decimalSIPrefix('H/s', 5) }, + { name: 'exahashes/sec', id: 'EHs', fn: decimalSIPrefix('H/s', 6) }, + ], + }, + { + name: 'Mass', + formats: [ + { name: 'milligram (mg)', id: 'massmg', fn: decimalSIPrefix('g', -1) }, + { name: 'gram (g)', id: 'massg', fn: decimalSIPrefix('g') }, + { name: 'kilogram (kg)', id: 'masskg', fn: decimalSIPrefix('g', 1) }, + { name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') }, + ], + }, + { + name: 'length', + formats: [ + { name: 'millimetre (mm)', id: 'lengthmm', fn: decimalSIPrefix('m', -1) }, + { name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') }, + { name: 'meter (m)', id: 'lengthm', fn: decimalSIPrefix('m') }, + { name: 'kilometer (km)', id: 'lengthkm', fn: decimalSIPrefix('m', 1) }, + { name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') }, + ], + }, + { + name: 'Pressure', + formats: [ + { name: 'Millibars', id: 'pressurembar', fn: decimalSIPrefix('bar', -1) }, + { name: 'Bars', id: 'pressurebar', fn: decimalSIPrefix('bar') }, + { name: 'Kilobars', id: 'pressurekbar', fn: decimalSIPrefix('bar', 1) }, + { name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') }, + { name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') }, + { name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') }, + { name: 'PSI', id: 'pressurepsi', fn: scaledUnits(1000, ['psi', 'ksi', 'Mpsi']) }, + ], + }, + { + name: 'Radiation', + formats: [ + { name: 'Becquerel (Bq)', id: 'radbq', fn: decimalSIPrefix('Bq') }, + { name: 'curie (Ci)', id: 'radci', fn: decimalSIPrefix('Ci') }, + { name: 'Gray (Gy)', id: 'radgy', fn: decimalSIPrefix('Gy') }, + { name: 'rad', id: 'radrad', fn: decimalSIPrefix('rad') }, + { name: 'Sievert (Sv)', id: 'radsv', fn: decimalSIPrefix('Sv') }, + { name: 'rem', id: 'radrem', fn: decimalSIPrefix('rem') }, + { name: 'Exposure (C/kg)', id: 'radexpckg', fn: decimalSIPrefix('C/kg') }, + { name: 'roentgen (R)', id: 'radr', fn: decimalSIPrefix('R') }, + { name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: decimalSIPrefix('Sv/h') }, + ], + }, + { + name: 'Temperature', + formats: [ + { name: 'Celsius (°C)', id: 'celsius', fn: toFixedUnit('°C') }, + { name: 'Farenheit (°F)', id: 'farenheit', fn: toFixedUnit('°F') }, + { name: 'Kelvin (K)', id: 'kelvin', fn: toFixedUnit('K') }, + ], + }, + { + name: 'Time', + formats: [ + { name: 'Hertz (1/s)', id: 'hertz', fn: decimalSIPrefix('Hz') }, + { name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds }, + { name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds }, + { name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds }, + { name: 'seconds (s)', id: 's', fn: toSeconds }, + { name: 'minutes (m)', id: 'm', fn: toMinutes }, + { name: 'hours (h)', id: 'h', fn: toHours }, + { name: 'days (d)', id: 'd', fn: toDays }, + { name: 'duration (ms)', id: 'dtdurationms', fn: toDurationInMilliseconds }, + { name: 'duration (s)', id: 'dtdurations', fn: toDurationInSeconds }, + { name: 'duration (hh:mm:ss)', id: 'dthms', fn: toDurationInHoursMinutesSeconds }, + { name: 'Timeticks (s/100)', id: 'timeticks', fn: toTimeTicks }, + { name: 'clock (ms)', id: 'clockms', fn: toClockMilliseconds }, + { name: 'clock (s)', id: 'clocks', fn: toClockSeconds }, + ], + }, + { + name: 'Throughput', + formats: [ + { name: 'ops/sec (ops)', id: 'ops', fn: simpleCountUnit('ops') }, + { name: 'requests/sec (rps)', id: 'reqps', fn: simpleCountUnit('reqps') }, + { name: 'reads/sec (rps)', id: 'rps', fn: simpleCountUnit('rps') }, + { name: 'writes/sec (wps)', id: 'wps', fn: simpleCountUnit('wps') }, + { name: 'I/O ops/sec (iops)', id: 'iops', fn: simpleCountUnit('iops') }, + { name: 'ops/min (opm)', id: 'opm', fn: simpleCountUnit('opm') }, + { name: 'reads/min (rpm)', id: 'rpm', fn: simpleCountUnit('rpm') }, + { name: 'writes/min (wpm)', id: 'wpm', fn: simpleCountUnit('wpm') }, + ], + }, + { + name: 'Velocity', + formats: [ + { name: 'metres/second (m/s)', id: 'velocityms', fn: toFixedUnit('m/s') }, + { name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') }, + { name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') }, + { name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') }, + ] + }, + { + name: 'Volume', + formats: [ + { name: 'millilitre (mL)', id: 'mlitre', fn: decimalSIPrefix('L', -1) }, + { name: 'litre (L)', id: 'litre', fn: decimalSIPrefix('L') }, + { name: 'cubic metre', id: 'm3', fn: toFixedUnit('m³') }, + { name: 'Normal cubic metre', id: 'Nm3', fn: toFixedUnit('Nm³') }, + { name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') }, + { name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') }, + ], + } +]; diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts new file mode 100644 index 00000000000..cf69a1d433a --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts @@ -0,0 +1,231 @@ +import moment from 'moment'; +import { + dateTimeAsIso, + dateTimeAsUS, + dateTimeFromNow, + Interval, + toClock, + toDuration, + toDurationInMilliseconds, + toDurationInSeconds, +} from './dateTimeFormatters'; + +describe('date time formats', () => { + const epoch = 1505634997920; + const utcTime = moment.utc(epoch); + const browserTime = moment(epoch); + + it('should format as iso date', () => { + const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); + const actual = dateTimeAsIso(epoch, 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC)', () => { + const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); + const actual = dateTimeAsIso(epoch, 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as iso date and skip date when today', () => { + const now = moment(); + const expected = now.format('HH:mm:ss'); + const actual = dateTimeAsIso(now.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC) and skip date when today', () => { + const now = moment.utc(); + const expected = now.format('HH:mm:ss'); + const actual = dateTimeAsIso(now.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as US date', () => { + const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); + const actual = dateTimeAsUS(epoch, 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC)', () => { + const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); + const actual = dateTimeAsUS(epoch, 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as US date and skip date when today', () => { + const now = moment(); + const expected = now.format('h:mm:ss a'); + const actual = dateTimeAsUS(now.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC) and skip date when today', () => { + const now = moment.utc(); + const expected = now.format('h:mm:ss a'); + const actual = dateTimeAsUS(now.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as from now with days', () => { + const daysAgo = moment().add(-7, 'd'); + const expected = '7 days ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as from now with days (in UTC)', () => { + const daysAgo = moment.utc().add(-7, 'd'); + const expected = '7 days ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes', () => { + const daysAgo = moment().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes (in UTC)', () => { + const daysAgo = moment.utc().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); +}); + +describe('duration', () => { + it('0 milliseconds', () => { + const str = toDurationInMilliseconds(0, 0); + expect(str).toBe('0 milliseconds'); + }); + it('1 millisecond', () => { + const str = toDurationInMilliseconds(1, 0); + expect(str).toBe('1 millisecond'); + }); + it('-1 millisecond', () => { + const str = toDurationInMilliseconds(-1, 0); + expect(str).toBe('1 millisecond ago'); + }); + it('seconds', () => { + const str = toDurationInSeconds(1, 0); + expect(str).toBe('1 second'); + }); + it('minutes', () => { + const str = toDuration(1, 0, Interval.Minute); + expect(str).toBe('1 minute'); + }); + it('hours', () => { + const str = toDuration(1, 0, Interval.Hour); + expect(str).toBe('1 hour'); + }); + it('days', () => { + const str = toDuration(1, 0, Interval.Day); + expect(str).toBe('1 day'); + }); + it('weeks', () => { + const str = toDuration(1, 0, Interval.Week); + expect(str).toBe('1 week'); + }); + it('months', () => { + const str = toDuration(1, 0, Interval.Month); + expect(str).toBe('1 month'); + }); + it('years', () => { + const str = toDuration(1, 0, Interval.Year); + expect(str).toBe('1 year'); + }); + it('decimal days', () => { + const str = toDuration(1.5, 2, Interval.Day); + expect(str).toBe('1 day, 12 hours, 0 minutes'); + }); + it('decimal months', () => { + const str = toDuration(1.5, 3, Interval.Month); + expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); + }); + it('no decimals', () => { + const str = toDuration(38898367008, 0, Interval.Millisecond); + expect(str).toBe('1 year'); + }); + it('1 decimal', () => { + const str = toDuration(38898367008, 1, Interval.Millisecond); + expect(str).toBe('1 year, 2 months'); + }); + it('too many decimals', () => { + const str = toDuration(38898367008, 20, Interval.Millisecond); + expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); + }); + it('floating point error', () => { + const str = toDuration(36993906007, 8, Interval.Millisecond); + expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); + }); +}); + +describe('clock', () => { + it('size less than 1 second', () => { + const str = toClock(999, 0); + expect(str).toBe('999ms'); + }); + describe('size less than 1 minute', () => { + it('default', () => { + const str = toClock(59999); + expect(str).toBe('59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(59999, 0); + expect(str).toBe('59s'); + }); + }); + describe('size less than 1 hour', () => { + it('default', () => { + const str = toClock(3599999); + expect(str).toBe('59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(3599999, 0); + expect(str).toBe('59m'); + }); + it('decimals equals 1', () => { + const str = toClock(3599999, 1); + expect(str).toBe('59m:59s'); + }); + }); + describe('size greater than or equal 1 hour', () => { + it('default', () => { + const str = toClock(7199999); + expect(str).toBe('01h:59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(7199999, 0); + expect(str).toBe('01h'); + }); + it('decimals equals 1', () => { + const str = toClock(7199999, 1); + expect(str).toBe('01h:59m'); + }); + it('decimals equals 2', () => { + const str = toClock(7199999, 2); + expect(str).toBe('01h:59m:59s'); + }); + }); + describe('size greater than or equal 1 day', () => { + it('default', () => { + const str = toClock(89999999); + expect(str).toBe('24h:59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(89999999, 0); + expect(str).toBe('24h'); + }); + it('decimals equals 1', () => { + const str = toClock(89999999, 1); + expect(str).toBe('24h:59m'); + }); + it('decimals equals 2', () => { + const str = toClock(89999999, 2); + expect(str).toBe('24h:59m:59s'); + }); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts new file mode 100644 index 00000000000..1e07857eb66 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts @@ -0,0 +1,312 @@ +import { toFixed, toFixedScaled } from './valueFormats'; +import moment from 'moment'; + +interface IntervalsInSeconds { + [interval: string]: number; +} + +export enum Interval { + Year = 'year', + Month = 'month', + Week = 'week', + Day = 'day', + Hour = 'hour', + Minute = 'minute', + Second = 'second', + Millisecond = 'millisecond', +} + +const INTERVALS_IN_SECONDS: IntervalsInSeconds = { + [Interval.Year]: 31536000, + [Interval.Month]: 2592000, + [Interval.Week]: 604800, + [Interval.Day]: 86400, + [Interval.Hour]: 3600, + [Interval.Minute]: 60, + [Interval.Second]: 1, + [Interval.Millisecond]: 0.001, +}; + +export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' ns'; + } else if (Math.abs(size) < 1000000) { + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); + } else if (Math.abs(size) < 1000000000) { + return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); + } else if (Math.abs(size) < 60000000000) { + return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); + } else { + return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); + } +} + +export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' µs'; + } else if (Math.abs(size) < 1000000) { + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); + } else { + return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s'); + } +} + +export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' ms'; + } else if (Math.abs(size) < 60000) { + // Less than 1 min + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); + } else if (Math.abs(size) < 3600000) { + // Less than 1 hour, divide in minutes + return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); + } else if (Math.abs(size) < 86400000) { + // Less than one day, divide in hours + return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); + } else if (Math.abs(size) < 31536000000) { + // Less than one year, divide in days + return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); + } + + return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); +} + +export function toSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + // Less than 1 µs, divide in ns + if (Math.abs(size) < 0.000001) { + return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); + } + // Less than 1 ms, divide in µs + if (Math.abs(size) < 0.001) { + return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); + } + // Less than 1 second, divide in ms + if (Math.abs(size) < 1) { + return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); + } + + if (Math.abs(size) < 60) { + return toFixed(size, decimals) + ' s'; + } else if (Math.abs(size) < 3600) { + // Less than 1 hour, divide in minutes + return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); + } else if (Math.abs(size) < 86400) { + // Less than one day, divide in hours + return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour'); + } else if (Math.abs(size) < 604800) { + // Less than one week, divide in days + return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); + } else if (Math.abs(size) < 31536000) { + // Less than one year, divide in week + return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); + } + + return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); +} + +export function toMinutes(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 60) { + return toFixed(size, decimals) + ' min'; + } else if (Math.abs(size) < 1440) { + return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); + } else if (Math.abs(size) < 10080) { + return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); + } else if (Math.abs(size) < 604800) { + return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); + } else { + return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); + } +} + +export function toHours(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 24) { + return toFixed(size, decimals) + ' hour'; + } else if (Math.abs(size) < 168) { + return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); + } else if (Math.abs(size) < 8760) { + return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week'); + } else { + return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year'); + } +} + +export function toDays(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 7) { + return toFixed(size, decimals) + ' day'; + } else if (Math.abs(size) < 365) { + return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); + } else { + return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year'); + } +} + +export function toDuration(size: number, decimals: number, timeScale: Interval): string { + if (size === null) { + return ''; + } + if (size === 0) { + return '0 ' + timeScale + 's'; + } + if (size < 0) { + return toDuration(-size, decimals, timeScale) + ' ago'; + } + + const units = [ + { long: Interval.Year }, + { long: Interval.Month }, + { long: Interval.Week }, + { long: Interval.Day }, + { long: Interval.Hour }, + { long: Interval.Minute }, + { long: Interval.Second }, + { long: Interval.Millisecond }, + ]; + // convert $size to milliseconds + // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors + size *= INTERVALS_IN_SECONDS[timeScale] * 1000; + + const strings = []; + // after first value >= 1 print only $decimals more + let decrementDecimals = false; + for (let i = 0; i < units.length && decimals >= 0; i++) { + const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000; + const value = size / interval; + if (value >= 1 || decrementDecimals) { + decrementDecimals = true; + const floor = Math.floor(value); + const unit = units[i].long + (floor !== 1 ? 's' : ''); + strings.push(floor + ' ' + unit); + size = size % interval; + decimals--; + } + } + + return strings.join(', '); +} + +export function toClock(size: number, decimals?: number) { + if (size === null) { + return ''; + } + + // < 1 second + if (size < 1000) { + return moment.utc(size).format('SSS\\m\\s'); + } + + // < 1 minute + if (size < 60000) { + let format = 'ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'ss\\s'; + } + return moment.utc(size).format(format); + } + + // < 1 hour + if (size < 3600000) { + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'mm\\m'; + } else if (decimals === 1) { + format = 'mm\\m:ss\\s'; + } + return moment.utc(size).format(format); + } + + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + + const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`; + + if (decimals === 0) { + format = ''; + } else if (decimals === 1) { + format = 'mm\\m'; + } else if (decimals === 2) { + format = 'mm\\m:ss\\s'; + } + + return format ? `${hours}:${moment.utc(size).format(format)}` : hours; +} + +export function toDurationInMilliseconds(size: number, decimals: number) { + return toDuration(size, decimals, Interval.Millisecond); +} + +export function toDurationInSeconds(size: number, decimals: number) { + return toDuration(size, decimals, Interval.Second); +} + +export function toDurationInHoursMinutesSeconds(size: number) { + const strings = []; + const numHours = Math.floor(size / 3600); + const numMinutes = Math.floor((size % 3600) / 60); + const numSeconds = Math.floor((size % 3600) % 60); + numHours > 9 ? strings.push('' + numHours) : strings.push('0' + numHours); + numMinutes > 9 ? strings.push('' + numMinutes) : strings.push('0' + numMinutes); + numSeconds > 9 ? strings.push('' + numSeconds) : strings.push('0' + numSeconds); + return strings.join(':'); +} + +export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) { + return toSeconds(size, decimals, scaledDecimals); +} + +export function toClockMilliseconds(size: number, decimals: number) { + return toClock(size, decimals); +} + +export function toClockSeconds(size: number, decimals: number) { + return toClock(size * 1000, decimals); +} + +export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + + if (moment().isSame(value, 'day')) { + return time.format('HH:mm:ss'); + } + return time.format('YYYY-MM-DD HH:mm:ss'); +} + +export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + + if (moment().isSame(value, 'day')) { + return time.format('h:mm:ss a'); + } + return time.format('MM/DD/YYYY h:mm:ss a'); +} + +export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + return time.fromNow(); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts new file mode 100644 index 00000000000..49278711608 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts @@ -0,0 +1,7 @@ +import { currency } from './symbolFormatters'; + +describe('Currency', () => { + it('should format as usd', () => { + expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts new file mode 100644 index 00000000000..66808143daa --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts @@ -0,0 +1,30 @@ +import { scaledUnits } from './valueFormats'; + +export function currency(symbol: string) { + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = scaledUnits(1000, units); + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + const scaled = scaler(size, decimals, scaledDecimals); + return symbol + scaled; + }; +} + +export function binarySIPrefix(unit: string, offset = 0) { + const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); + const units = prefixes.map(p => { + return ' ' + p + unit; + }); + return scaledUnits(1024, units); +} + +export function decimalSIPrefix(unit: string, offset = 0) { + let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + prefixes = prefixes.slice(3 + (offset || 0)); + const units = prefixes.map(p => { + return ' ' + p + unit; + }); + return scaledUnits(1000, units); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts new file mode 100644 index 00000000000..0a56ce58e5b --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts @@ -0,0 +1,166 @@ +import { getCategories } from './categories'; + +type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string; + +interface ValueFormat { + name: string; + id: string; + fn: ValueFormatter; +} + +export interface ValueFormatCategory { + name: string; + formats: ValueFormat[]; +} + +interface ValueFormatterIndex { + [id: string]: ValueFormatter; +} + +// Globals & formats cache +let categories: ValueFormatCategory[] = []; +const index: ValueFormatterIndex = {}; +let hasBuiltIndex = false; + +export function toFixed(value: number, decimals?: number): string { + if (value === null) { + return ''; + } + + const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; + const formatted = String(Math.round(value * factor) / factor); + + // if exponent return directly + if (formatted.indexOf('e') !== -1 || value === 0) { + return formatted; + } + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + if (decimals != null) { + const decimalPos = formatted.indexOf('.'); + const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; + if (precision < decimals) { + return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); + } + } + + return formatted; +} + +export function toFixedScaled( + value: number, + decimals: number, + scaledDecimals: number, + additionalDecimals: number, + ext: string +) { + if (scaledDecimals === null) { + return toFixed(value, decimals) + ext; + } else { + return toFixed(value, scaledDecimals + additionalDecimals) + ext; + } +} + +export function toFixedUnit(unit: string) { + return (size: number, decimals: number) => { + if (size === null) { + return ''; + } + return toFixed(size, decimals) + ' ' + unit; + }; +} + +// Formatter which scales the unit string geometrically according to the given +// numeric factor. Repeatedly scales the value down by the factor until it is +// less than the factor in magnitude, or the end of the array is reached. +export function scaledUnits(factor: number, extArray: string[]) { + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + + let steps = 0; + const limit = extArray.length; + + while (Math.abs(size) >= factor) { + steps++; + size /= factor; + + if (steps >= limit) { + return 'NA'; + } + } + + if (steps > 0 && scaledDecimals !== null) { + decimals = scaledDecimals + 3 * steps; + } + + return toFixed(size, decimals) + extArray[steps]; + }; +} + +export function locale(value: number, decimals: number) { + if (value == null) { + return ''; + } + return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); +} + +export function simpleCountUnit(symbol: string) { + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = scaledUnits(1000, units); + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + const scaled = scaler(size, decimals, scaledDecimals); + return scaled + ' ' + symbol; + }; +} + +function buildFormats() { + categories = getCategories(); + + for (const cat of categories) { + for (const format of cat.formats) { + index[format.id] = format.fn; + } + } + + hasBuiltIndex = true; +} + +export function getValueFormat(id: string): ValueFormatter { + if (!hasBuiltIndex) { + buildFormats(); + } + + return index[id]; +} + +export function getValueFormatterIndex(): ValueFormatterIndex { + if (!hasBuiltIndex) { + buildFormats(); + } + + return index; +} + +export function getValueFormats() { + if (!hasBuiltIndex) { + buildFormats(); + } + + return categories.map(cat => { + return { + text: cat.name, + submenu: cat.formats.map(format => { + return { + text: format.name, + value: format.id, + }; + }), + }; + }); +} diff --git a/packages/grafana-ui/src/visualizations/index.ts b/packages/grafana-ui/src/visualizations/index.ts deleted file mode 100644 index 967432d37c9..00000000000 --- a/packages/grafana-ui/src/visualizations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Graph } from './Graph/Graph'; diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 4d4f6539972..d4f2f2aa7a3 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM debian:stretch-slim +ARG BASE_IMAGE=debian:stretch-slim +FROM ${BASE_IMAGE} ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz" @@ -10,7 +11,8 @@ COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana -FROM debian:stretch-slim +ARG BASE_IMAGE=debian:stretch-slim +FROM ${BASE_IMAGE} ARG GF_UID="472" ARG GF_GID="472" diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh index ac3226a4a61..22655bead8c 100755 --- a/packaging/docker/build-deploy.sh +++ b/packaging/docker/build-deploy.sh @@ -8,6 +8,5 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" ./push_to_docker_hub.sh "$_grafana_version" if echo "$_grafana_version" | grep -q "^master-"; then - apk add --no-cache curl ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version" fi diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index c303c71cd5f..a522363089b 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -1,25 +1,49 @@ #!/bin/sh -_grafana_tag=$1 +_grafana_tag=${1:-} +_docker_repo=${2:-grafana/grafana} # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _docker_repo=${2:-grafana/grafana-dev} fi echo "Building ${_docker_repo}:${_grafana_version}" -docker build \ - --tag "${_docker_repo}:${_grafana_version}" \ - --no-cache=true . +export DOCKER_CLI_EXPERIMENTAL=enabled + +# Build grafana image for a specific arch +docker_build () { + base_image=$1 + grafana_tgz=$2 + tag=$3 + + docker build \ + --build-arg BASE_IMAGE=${base_image} \ + --build-arg GRAFANA_TGZ=${grafana_tgz} \ + --tag "${tag}" \ + --no-cache=true . +} + +# Tag docker images of all architectures +docker_tag_all () { + repo=$1 + tag=$2 + docker tag "${_docker_repo}:${_grafana_version}" "${repo}:${tag}" + docker tag "${_docker_repo}-arm32v7-linux:${_grafana_version}" "${repo}-arm32v7-linux:${tag}" + docker tag "${_docker_repo}-arm64v8-linux:${_grafana_version}" "${repo}-arm64v8-linux:${tag}" +} + +docker_build "debian:stretch-slim" "grafana-latest.linux-x64.tar.gz" "${_docker_repo}:${_grafana_version}" +docker_build "arm32v7/debian:stretch-slim" "grafana-latest.linux-armv7.tar.gz" "${_docker_repo}-arm32v7-linux:${_grafana_version}" +docker_build "arm64v8/debian:stretch-slim" "grafana-latest.linux-arm64.tar.gz" "${_docker_repo}-arm64v8-linux:${_grafana_version}" # Tag as 'latest' for official release; otherwise tag as grafana/grafana:master if echo "$_grafana_tag" | grep -q "^v"; then - docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest" + docker_tag_all "${_docker_repo}" "latest" else - docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master" + docker_tag_all "${_docker_repo}" "master" + docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana-dev:${_grafana_version}" fi diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh index 526c216f8fa..37b5ae0095c 100755 --- a/packaging/docker/push_to_docker_hub.sh +++ b/packaging/docker/push_to_docker_hub.sh @@ -1,24 +1,46 @@ #!/bin/sh set -e -_grafana_tag=$1 +_grafana_tag=${1:-} +_docker_repo=${2:-grafana/grafana} # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _docker_repo=${2:-grafana/grafana-dev} fi +export DOCKER_CLI_EXPERIMENTAL=enabled + echo "pushing ${_docker_repo}:${_grafana_version}" -docker push "${_docker_repo}:${_grafana_version}" + + +docker_push_all () { + repo=$1 + tag=$2 + + # Push each image individually + docker push "${repo}:${tag}" + docker push "${repo}-arm32v7-linux:${tag}" + docker push "${repo}-arm64v8-linux:${tag}" + + # Create and push a multi-arch manifest + docker manifest create "${repo}:${tag}" \ + "${repo}:${tag}" \ + "${repo}-arm32v7-linux:${tag}" \ + "${repo}-arm64v8-linux:${tag}" + + docker manifest push "${repo}:${tag}" +} if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then echo "pushing ${_docker_repo}:latest" - docker push "${_docker_repo}:latest" + docker_push_all "${_docker_repo}" "latest" + docker_push_all "${_docker_repo}" "${_grafana_version}" +elif echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -q "beta"; then + docker_push_all "${_docker_repo}" "${_grafana_version}" elif echo "$_grafana_tag" | grep -q "master"; then - echo "pushing grafana/grafana:master" - docker push grafana/grafana:master + docker_push_all "${_docker_repo}" "master" + docker push "grafana/grafana-dev:${_grafana_version}" fi diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 66b3b504946..19fb4efd7e8 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -212,6 +212,10 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } + if query.Result == nil { + return Error(404, "Alert notification not found", nil) + } + return JSON(200, dtos.NewAlertNotification(query.Result)) } diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 331beeef5e4..168193e377f 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -119,6 +119,12 @@ func TestAlertingApiEndpoint(t *testing.T) { So(getAlertsQuery.Limit, ShouldEqual, 5) So(getAlertsQuery.Query, ShouldEqual, "alertQuery") }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alert-notifications/1", "/alert-notifications/:notificationId", m.ROLE_ADMIN, func(sc *scenarioContext) { + sc.handlerFunc = GetAlertNotificationByID + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + }) }) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 3a8010e797b..0f3f56175fe 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard { func NewDashboardFolder(title string) *Dashboard { folder := NewDashboard(title) folder.IsFolder = true - folder.Data.Set("schemaVersion", 16) + folder.Data.Set("schemaVersion", 17) folder.Data.Set("version", 0) folder.IsFolder = true return folder diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e33e3dc2af3..5b911c5a9ad 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -112,7 +112,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()) if err != nil { - return nil, ValidationError{Reason: "Could not parse frequency"} + return nil, ValidationError{Reason: err.Error()} } rawFor := jsonAlert.Get("for").MustString() diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index d2a505145ac..4423046d600 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -1,16 +1,21 @@ package alerting import ( + "errors" "fmt" "regexp" "strconv" "time" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" ) +var ( + ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`) + ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`) +) + type Rule struct { Id int64 OrgId int64 @@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) { matches := ValueFormatRegex.FindAllString(str, 1) if len(matches) <= 0 { - return 0, fmt.Errorf("Frequency could not be parsed") + return 0, ErrFrequencyCouldNotBeParsed } value, err := strconv.Atoi(matches[0]) @@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) { return 0, err } + if value == 0 { + return 0, ErrFrequencyCannotBeZeroOrLess + } + unit := UnitFormatRegex.FindAllString(str, 1)[0] if val, ok := unitMultiplier[unit]; ok { @@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.PanelId = ruleDef.PanelId model.Name = ruleDef.Name model.Message = ruleDef.Message - model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate model.For = ruleDef.For @@ -109,6 +117,13 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges + model.Frequency = ruleDef.Frequency + // frequency cannot be zero since that would not execute the alert rule. + // so we fallback to 60 seconds if `Freqency` is missing + if model.Frequency == 0 { + model.Frequency = 60 + } + for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) id, err := jsonModel.Get("id").Int64() diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 2a9e95e5723..cf25cc118f4 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -14,6 +14,36 @@ func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) { return &ConditionResult{}, nil } +func TestAlertRuleFrequencyParsing(t *testing.T) { + tcs := []struct { + input string + err error + result int64 + }{ + {input: "10s", result: 10}, + {input: "10m", result: 600}, + {input: "1h", result: 3600}, + {input: "1o", result: 1}, + {input: "0s", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0m", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0h", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "-1s", err: ErrFrequencyCouldNotBeParsed}, + } + + for _, tc := range tcs { + r, err := getTimeDurationStringToSeconds(tc.input) + if err != tc.err { + t.Errorf("expected error: '%v' got: '%v'", tc.err, err) + return + } + + if r != tc.result { + t.Errorf("expected result: %d got %d", tc.result, r) + } + } +} + func TestAlertRuleModel(t *testing.T) { Convey("Testing alert rule", t, func() { @@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) { return &FakeCondition{}, nil }) - Convey("Can parse seconds", func() { - seconds, _ := getTimeDurationStringToSeconds("10s") - So(seconds, ShouldEqual, 10) - }) - - Convey("Can parse minutes", func() { - seconds, _ := getTimeDurationStringToSeconds("10m") - So(seconds, ShouldEqual, 600) - }) - - Convey("Can parse hours", func() { - seconds, _ := getTimeDurationStringToSeconds("1h") - So(seconds, ShouldEqual, 3600) - }) - - Convey("defaults to seconds", func() { - seconds, _ := getTimeDurationStringToSeconds("1o") - So(seconds, ShouldEqual, 1) - }) - Convey("should return err for empty string", func() { _, err := getTimeDurationStringToSeconds("") So(err, ShouldNotBeNil) @@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) { So(len(alertRule.Notifications), ShouldEqual, 2) }) }) + + Convey("can construct alert rule model with invalid frequency", func() { + json := ` + { + "name": "name2", + "description": "desc2", + "noDataMode": "critical", + "enabled": true, + "frequency": "0s", + "conditions": [ { "type": "test", "prop": 123 } ], + "notifications": [] + }` + + alertJSON, jsonErr := simplejson.NewJson([]byte(json)) + So(jsonErr, ShouldBeNil) + + alert := &m.Alert{ + Id: 1, + OrgId: 1, + DashboardId: 1, + PanelId: 1, + Frequency: 0, + + Settings: alertJSON, + } + + alertRule, err := NewRuleFromDBAlert(alert) + So(err, ShouldBeNil) + So(alertRule.Frequency, ShouldEqual, 60) + }) }) } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index dbe441c915e..2be3b145372 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -3,6 +3,7 @@ package notifications import ( "bytes" "context" + "crypto/tls" "fmt" "io" "io/ioutil" @@ -26,6 +27,9 @@ type Webhook struct { } var netTransport = &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateFreelyAsClient, + }, Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 5609c058a27..d6fc68293c3 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -6,6 +6,7 @@ import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; import AppNotificationList from './components/AppNotifications/AppNotificationList'; +import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -19,4 +20,13 @@ export function registerAngularDirectives() { ['onChange', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); + react2AngularDirective('colorPicker', ColorPicker, [ + 'color', + ['onChange', { watchDepth: 'reference', wrapApply: true }], + ]); + react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [ + 'series', + 'onColorChange', + 'onToggleAxis', + ]); } diff --git a/public/app/core/components/Label/Label.tsx b/public/app/core/components/Label/Label.tsx index 362c3c577f7..5d60efa056a 100644 --- a/public/app/core/components/Label/Label.tsx +++ b/public/app/core/components/Label/Label.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode } from 'react'; -import Tooltip from '../Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface Props { tooltip?: string; @@ -14,8 +14,10 @@ export const Label: SFC = props => { {props.children} {props.tooltip && ( - - + +
+ +
)}
diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 749bef680bf..30219371257 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker'; -import { Select, SelectOptionItem } from 'app/core/components/Select/Select'; +import { Select, SelectOptionItem } from '@grafana/ui'; import { User } from 'app/types'; import { dashboardPermissionLevels, diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx index d3f9ddbb1fb..ebf3cbad1bc 100644 --- a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import Select from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { dashboardPermissionLevels } from 'app/types/acl'; export interface Props { diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx index e726667cfbb..c33b564154a 100644 --- a/public/app/core/components/PermissionList/PermissionListItem.tsx +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { Select } from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl'; import { FolderInfo } from 'app/types'; diff --git a/public/app/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx deleted file mode 100644 index 24d17f67367..00000000000 --- a/public/app/core/components/ScrollBar/ScrollBar.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import baron from 'baron'; - -export interface Props { - children: any; - className: string; -} - -export default class ScrollBar extends React.Component { - private container: any; - private scrollbar: baron; - - constructor(props) { - super(props); - } - - componentDidMount() { - this.scrollbar = baron({ - root: this.container.parentElement, - scroller: this.container, - bar: '.baron__bar', - barOnCls: '_scrollbar', - scrollingCls: '_scrolling', - track: '.baron__track', - }); - } - - componentDidUpdate() { - this.scrollbar.update(); - } - - componentWillUnmount() { - this.scrollbar.dispose(); - } - - // methods can be invoked by outside - setScrollTop(top) { - if (this.container) { - this.container.scrollTop = top; - this.scrollbar.update(); - - return true; - } - return false; - } - - setScrollLeft(left) { - if (this.container) { - this.container.scrollLeft = left; - this.scrollbar.update(); - - return true; - } - return false; - } - - update() { - this.scrollbar.update(); - } - - handleRef = ref => { - this.container = ref; - }; - - render() { - return ( -
-
- {this.props.children} -
- -
-
-
-
- ); - } -} diff --git a/public/app/core/components/Select/DataSourcePicker.tsx b/public/app/core/components/Select/DataSourcePicker.tsx index 1a9081038c0..372c4cd4013 100644 --- a/public/app/core/components/Select/DataSourcePicker.tsx +++ b/public/app/core/components/Select/DataSourcePicker.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components -import Select from './Select'; +import { Select } from '@grafana/ui'; // Types import { DataSourceSelectItem } from 'app/types'; diff --git a/public/app/core/components/Select/ResetStyles.tsx b/public/app/core/components/Select/ResetStyles.tsx deleted file mode 100644 index c34abb544ab..00000000000 --- a/public/app/core/components/Select/ResetStyles.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default { - clearIndicator: () => ({}), - container: () => ({}), - control: () => ({}), - dropdownIndicator: () => ({}), - group: () => ({}), - groupHeading: () => ({}), - indicatorsContainer: () => ({}), - indicatorSeparator: () => ({}), - input: () => ({}), - loadingIndicator: () => ({}), - loadingMessage: () => ({}), - menu: () => ({}), - menuList: ({ maxHeight }: { maxHeight: number }) => ({ - maxHeight, - }), - multiValue: () => ({}), - multiValueLabel: () => ({}), - multiValueRemove: () => ({}), - noOptionsMessage: () => ({}), - option: () => ({}), - placeholder: () => ({}), - singleValue: () => ({}), - valueContainer: () => ({}), -}; diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx index bc608318806..8d9e1d48d81 100644 --- a/public/app/core/components/Select/TeamPicker.tsx +++ b/public/app/core/components/Select/TeamPicker.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import _ from 'lodash'; -import { AsyncSelect } from './Select'; +import { AsyncSelect } from '@grafana/ui'; import { debounce } from 'lodash'; import { getBackendSrv } from 'app/core/services/backend_srv'; diff --git a/public/app/core/components/Select/UnitPicker.tsx b/public/app/core/components/Select/UnitPicker.tsx index 75885cbbb84..f9dbc0ae421 100644 --- a/public/app/core/components/Select/UnitPicker.tsx +++ b/public/app/core/components/Select/UnitPicker.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import Select from './Select'; -import kbn from 'app/core/utils/kbn'; +import { getValueFormats } from '@grafana/ui'; +import { Select } from '@grafana/ui'; interface Props { onChange: (item: any) => void; @@ -16,7 +16,7 @@ export default class UnitPicker extends PureComponent { render() { const { defaultValue, onChange, width } = this.props; - const unitGroups = kbn.getUnitFormats(); + const unitGroups = getValueFormats(); // Need to transform the data structure to work well with Select const groupOptions = unitGroups.map(group => { diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 8496d707105..ff4ae32f068 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; // Components -import { AsyncSelect } from './Select'; +import { AsyncSelect } from '@grafana/ui'; // Utils & Services import { debounce } from 'lodash'; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index d41626d9a2f..b13393ab2e1 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { Label } from 'app/core/components/Label/Label'; -import Select from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit } from 'app/types'; diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 4b2de6b1b16..7e8bc9c6fd2 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -1,12 +1,10 @@ import React from 'react'; +import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui'; import AsyncSelect from '@torkelo/react-select/lib/Async'; import { TagOption } from './TagOption'; import { TagBadge } from './TagBadge'; -import IndicatorsContainer from 'app/core/components/Select/IndicatorsContainer'; -import NoOptionsMessage from 'app/core/components/Select/NoOptionsMessage'; import { components } from '@torkelo/react-select'; -import ResetStyles from 'app/core/components/Select/ResetStyles'; export interface Props { tags: string[]; @@ -51,7 +49,7 @@ export class TagFilter extends React.Component { getOptionValue: i => i.value, getOptionLabel: i => i.label, value: tags, - styles: ResetStyles, + styles: resetSelectStyles(), filterOption: (option, searchQuery) => { const regex = RegExp(searchQuery, 'i'); return regex.test(option.value); diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 2524a265054..86e15923bda 100644 --- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode, PureComponent } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface ToggleButtonGroupProps { label?: string; diff --git a/public/app/core/components/Tooltip/Popover.test.tsx b/public/app/core/components/Tooltip/Popover.test.tsx deleted file mode 100644 index eea416a9262..00000000000 --- a/public/app/core/components/Tooltip/Popover.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import Popover from './Popover'; - -describe('Popover', () => { - it('renders correctly', () => { - const tree = renderer - .create( - - - - ) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/Tooltip/Popover.tsx b/public/app/core/components/Tooltip/Popover.tsx deleted file mode 100644 index 62397243c1c..00000000000 --- a/public/app/core/components/Tooltip/Popover.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { PureComponent } from 'react'; -import Popper from './Popper'; -import withPopper, { UsingPopperProps } from './withPopper'; - -class Popover extends PureComponent { - render() { - const { children, hidePopper, showPopper, className, ...restProps } = this.props; - - const togglePopper = restProps.show ? hidePopper : showPopper; - - return ( -
- {children} -
- ); - } -} - -export default withPopper(Popover); diff --git a/public/app/core/components/Tooltip/Tooltip.tsx b/public/app/core/components/Tooltip/Tooltip.tsx deleted file mode 100644 index 795da94a03c..00000000000 --- a/public/app/core/components/Tooltip/Tooltip.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { PureComponent } from 'react'; -import Popper from './Popper'; -import withPopper, { UsingPopperProps } from './withPopper'; - -class Tooltip extends PureComponent { - render() { - const { children, hidePopper, showPopper, className, ...restProps } = this.props; - - return ( -
- {children} -
- ); - } -} - -export default withPopper(Tooltip); diff --git a/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap deleted file mode 100644 index b36a4fe9af9..00000000000 --- a/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Popover renders correctly 1`] = ` -
-
- -
-
-`; diff --git a/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap deleted file mode 100644 index c7d680049f4..00000000000 --- a/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Tooltip renders correctly 1`] = ` - -`; diff --git a/public/app/core/components/Tooltip/withPopper.tsx b/public/app/core/components/Tooltip/withPopper.tsx deleted file mode 100644 index 3766b78f0f6..00000000000 --- a/public/app/core/components/Tooltip/withPopper.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { Themes } from './Popper'; -export interface UsingPopperProps { - showPopper: (prevState: object) => void; - hidePopper: (prevState: object) => void; - renderContent: (content: any) => any; - show: boolean; - placement?: string; - content: string | ((props: any) => JSX.Element); - className?: string; - refClassName?: string; - theme?: Themes; -} - -interface Props { - placement?: string; - className?: string; - refClassName?: string; - content: string | ((props: any) => JSX.Element); - theme?: Themes; -} - -interface State { - placement: string; - show: boolean; -} - -export default function withPopper(WrappedComponent) { - return class extends React.Component { - constructor(props) { - super(props); - this.setState = this.setState.bind(this); - this.state = { - placement: this.props.placement || 'auto', - show: false, - }; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.placement && nextProps.placement !== this.state.placement) { - this.setState(prevState => { - return { - ...prevState, - placement: nextProps.placement, - }; - }); - } - } - - showPopper = () => { - this.setState(prevState => ({ - ...prevState, - show: true, - })); - }; - - hidePopper = () => { - this.setState(prevState => ({ - ...prevState, - show: false, - })); - }; - - renderContent(content) { - if (typeof content === 'function') { - // If it's a function we assume it's a React component - const ReactComponent = content; - return ; - } - return content; - } - - render() { - const { show, placement } = this.state; - const className = this.props.className || ''; - return ( - - ); - } - }; -} diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 257a2077c97..6713d8bcd14 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -13,11 +13,10 @@ import './partials'; import './components/jsontree/jsontree'; import './components/code_editor/code_editor'; import './utils/outline'; -import './components/colorpicker/ColorPicker'; -import './components/colorpicker/SeriesColorPickerPopover'; import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; +import { colors } from '@grafana/ui/'; import { searchDirective } from './components/search/search'; import { infoPopover } from './components/info_popover'; @@ -36,7 +35,6 @@ import 'app/core/services/all'; import './filters/filters'; import coreModule from './core_module'; import appEvents from './app_events'; -import colors from './utils/colors'; import { assignModelProperties } from './utils/model_utils'; import { contextSrv } from './services/context_srv'; import { KeybindingSrv } from './services/keybindingSrv'; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 4e8c6207959..4cf9a029a2a 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,6 +1,8 @@ import _ from 'lodash'; +import { colors } from '@grafana/ui'; + import { TimeSeries } from 'app/core/core'; -import colors, { getThemeColor } from 'app/core/utils/colors'; +import { getThemeColor } from 'app/core/utils/colors'; /** * Mapping of log level abbreviation to canonical log level. diff --git a/public/app/core/specs/factors.test.ts b/public/app/core/specs/factors.test.ts new file mode 100644 index 00000000000..aed59b5be8b --- /dev/null +++ b/public/app/core/specs/factors.test.ts @@ -0,0 +1,8 @@ +import getFactors from 'app/core/utils/factors'; + +describe('factors', () => { + it('should return factors for 12', () => { + const factors = getFactors(12); + expect(factors).toEqual([1, 2, 3, 4, 6, 12]); + }); +}); diff --git a/public/app/core/specs/kbn.test.ts b/public/app/core/specs/kbn.test.ts deleted file mode 100644 index e621cdef632..00000000000 --- a/public/app/core/specs/kbn.test.ts +++ /dev/null @@ -1,493 +0,0 @@ -import kbn from '../utils/kbn'; -import * as dateMath from '../utils/datemath'; -import moment from 'moment'; - -describe('unit format menu', () => { - const menu = kbn.getUnitFormats(); - menu.map(submenu => { - describe('submenu ' + submenu.text, () => { - it('should have a title', () => { - expect(typeof submenu.text).toBe('string'); - }); - - it('should have a submenu', () => { - expect(Array.isArray(submenu.submenu)).toBe(true); - }); - - submenu.submenu.map(entry => { - describe('entry ' + entry.text, () => { - it('should have a title', () => { - expect(typeof entry.text).toBe('string'); - }); - it('should have a format', () => { - expect(typeof entry.value).toBe('string'); - }); - it('should have a valid format', () => { - expect(typeof kbn.valueFormats[entry.value]).toBe('function'); - }); - }); - }); - }); - }); -}); - -function describeValueFormat(desc, value, tickSize, tickDecimals, result) { - describe('value format: ' + desc, () => { - it('should translate ' + value + ' as ' + result, () => { - const scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); - const str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); - expect(str).toBe(result); - }); - }); -} - -describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms'); -describeValueFormat('ms', 100, 1, 0, '100 ms'); -describeValueFormat('ms', 1250, 10, 0, '1.25 s'); -describeValueFormat('ms', 1250, 300, 0, '1.3 s'); -describeValueFormat('ms', 65150, 10000, 0, '1.1 min'); -describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour'); -describeValueFormat('ms', 651500000, 150000000, 0, '8 day'); - -describeValueFormat('none', 2.75e-10, 0, 10, '3e-10'); -describeValueFormat('none', 0, 0, 2, '0'); -describeValueFormat('dB', 10, 1000, 2, '10.00 dB'); - -describeValueFormat('percent', 0, 0, 0, '0%'); -describeValueFormat('percent', 53, 0, 1, '53.0%'); -describeValueFormat('percentunit', 0.0, 0, 0, '0%'); -describeValueFormat('percentunit', 0.278, 0, 1, '27.8%'); -describeValueFormat('percentunit', 1.0, 0, 0, '100%'); - -describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42'); -describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K'); -describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M'); - -describeValueFormat('bytes', -1.57e308, -1.57e308, 2, 'NA'); - -describeValueFormat('ns', 25, 1, 0, '25 ns'); -describeValueFormat('ns', 2558, 50, 0, '2.56 µs'); - -describeValueFormat('ops', 123, 1, 0, '123 ops'); -describeValueFormat('rps', 456000, 1000, -1, '456K rps'); -describeValueFormat('rps', 123456789, 1000000, 2, '123.457M rps'); -describeValueFormat('wps', 789000000, 1000000, -1, '789M wps'); -describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops'); - -describeValueFormat('s', 1.23456789e-7, 1e-10, 8, '123.5 ns'); -describeValueFormat('s', 1.23456789e-4, 1e-7, 5, '123.5 µs'); -describeValueFormat('s', 1.23456789e-3, 1e-6, 4, '1.235 ms'); -describeValueFormat('s', 1.23456789e-2, 1e-5, 3, '12.35 ms'); -describeValueFormat('s', 1.23456789e-1, 1e-4, 2, '123.5 ms'); -describeValueFormat('s', 24, 1, 0, '24 s'); -describeValueFormat('s', 246, 1, 0, '4.1 min'); -describeValueFormat('s', 24567, 100, 0, '6.82 hour'); -describeValueFormat('s', 24567890, 10000, 0, '40.62 week'); -describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year'); - -describeValueFormat('m', 24, 1, 0, '24 min'); -describeValueFormat('m', 246, 10, 0, '4.1 hour'); -describeValueFormat('m', 6545, 10, 0, '4.55 day'); -describeValueFormat('m', 24567, 100, 0, '2.44 week'); -describeValueFormat('m', 24567892, 10000, 0, '46.7 year'); - -describeValueFormat('h', 21, 1, 0, '21 hour'); -describeValueFormat('h', 145, 1, 0, '6.04 day'); -describeValueFormat('h', 1234, 100, 0, '7.3 week'); -describeValueFormat('h', 9458, 1000, 0, '1.08 year'); - -describeValueFormat('d', 3, 1, 0, '3 day'); -describeValueFormat('d', 245, 100, 0, '35 week'); -describeValueFormat('d', 2456, 10, 0, '6.73 year'); - -describe('date time formats', () => { - const epoch = 1505634997920; - const utcTime = moment.utc(epoch); - const browserTime = moment(epoch); - - it('should format as iso date', () => { - const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(epoch); - expect(actual).toBe(expected); - }); - - it('should format as iso date (in UTC)', () => { - const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(epoch, true); - expect(actual).toBe(expected); - }); - - it('should format as iso date and skip date when today', () => { - const now = moment(); - const expected = now.format('HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as iso date (in UTC) and skip date when today', () => { - const now = moment.utc(); - const expected = now.format('HH:mm:ss'); - const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true); - expect(actual).toBe(expected); - }); - - it('should format as US date', () => { - const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(epoch, false); - expect(actual).toBe(expected); - }); - - it('should format as US date (in UTC)', () => { - const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(epoch, true); - expect(actual).toBe(expected); - }); - - it('should format as US date and skip date when today', () => { - const now = moment(); - const expected = now.format('h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as US date (in UTC) and skip date when today', () => { - const now = moment.utc(); - const expected = now.format('h:mm:ss a'); - const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true); - expect(actual).toBe(expected); - }); - - it('should format as from now with days', () => { - const daysAgo = moment().add(-7, 'd'); - const expected = '7 days ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as from now with days (in UTC)', () => { - const daysAgo = moment.utc().add(-7, 'd'); - const expected = '7 days ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); - expect(actual).toBe(expected); - }); - - it('should format as from now with minutes', () => { - const daysAgo = moment().add(-2, 'm'); - const expected = '2 minutes ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); - expect(actual).toBe(expected); - }); - - it('should format as from now with minutes (in UTC)', () => { - const daysAgo = moment.utc().add(-2, 'm'); - const expected = '2 minutes ago'; - const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); - expect(actual).toBe(expected); - }); -}); - -describe('kbn.toFixed and negative decimals', () => { - it('should treat as zero decimals', () => { - const str = kbn.toFixed(186.123, -2); - expect(str).toBe('186'); - }); -}); - -describe('kbn ms format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = kbn.valueFormats['ms'](10000086.123, 1, null); - expect(str).toBe('2.8 hour'); - }); -}); - -describe('kbn kbytes format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = kbn.valueFormats['kbytes'](10000000, 3, null); - expect(str).toBe('9.537 GiB'); - }); -}); - -describe('kbn deckbytes format when scaled decimals is null do not use it', () => { - it('should use specified decimals', () => { - const str = kbn.valueFormats['deckbytes'](10000000, 3, null); - expect(str).toBe('10.000 GB'); - }); -}); - -describe('kbn roundValue', () => { - it('should should handle null value', () => { - const str = kbn.roundValue(null, 2); - expect(str).toBe(null); - }); - it('should round value', () => { - const str = kbn.roundValue(200.877, 2); - expect(str).toBe(200.88); - }); -}); - -describe('calculateInterval', () => { - it('1h 100 resultion', () => { - const range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 100, null); - expect(res.interval).toBe('30s'); - }); - - it('10m 1600 resolution', () => { - const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1600, null); - expect(res.interval).toBe('500ms'); - expect(res.intervalMs).toBe(500); - }); - - it('fixed user min interval', () => { - const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1600, '10s'); - expect(res.interval).toBe('10s'); - expect(res.intervalMs).toBe(10000); - }); - - it('short time range and user low limit', () => { - const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1600, '>10s'); - expect(res.interval).toBe('10s'); - }); - - it('large time range and user low limit', () => { - const range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1000, '>10s'); - expect(res.interval).toBe('20m'); - }); - - it('10s 900 resolution and user low limit in ms', () => { - const range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 900, '>15ms'); - expect(res.interval).toBe('15ms'); - }); - - it('1d 1 resolution', () => { - const range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; - const res = kbn.calculateInterval(range, 1, null); - expect(res.interval).toBe('1d'); - expect(res.intervalMs).toBe(86400000); - }); - - it('86399s 1 resolution', () => { - const range = { - from: dateMath.parse('now-86390s'), - to: dateMath.parse('now'), - }; - const res = kbn.calculateInterval(range, 1, null); - expect(res.interval).toBe('12h'); - expect(res.intervalMs).toBe(43200000); - }); -}); - -describe('hex', () => { - it('positive integer', () => { - const str = kbn.valueFormats.hex(100, 0); - expect(str).toBe('64'); - }); - it('negative integer', () => { - const str = kbn.valueFormats.hex(-100, 0); - expect(str).toBe('-64'); - }); - it('null', () => { - const str = kbn.valueFormats.hex(null, 0); - expect(str).toBe(''); - }); - it('positive float', () => { - const str = kbn.valueFormats.hex(50.52, 1); - expect(str).toBe('32.8'); - }); - it('negative float', () => { - const str = kbn.valueFormats.hex(-50.333, 2); - expect(str).toBe('-32.547AE147AE14'); - }); -}); - -describe('hex 0x', () => { - it('positive integeter', () => { - const str = kbn.valueFormats.hex0x(7999, 0); - expect(str).toBe('0x1F3F'); - }); - it('negative integer', () => { - const str = kbn.valueFormats.hex0x(-584, 0); - expect(str).toBe('-0x248'); - }); - it('null', () => { - const str = kbn.valueFormats.hex0x(null, 0); - expect(str).toBe(''); - }); - it('positive float', () => { - const str = kbn.valueFormats.hex0x(74.443, 3); - expect(str).toBe('0x4A.716872B020C4'); - }); - it('negative float', () => { - const str = kbn.valueFormats.hex0x(-65.458, 1); - expect(str).toBe('-0x41.8'); - }); -}); - -describe('duration', () => { - it('null', () => { - const str = kbn.toDuration(null, 0, 'millisecond'); - expect(str).toBe(''); - }); - it('0 milliseconds', () => { - const str = kbn.toDuration(0, 0, 'millisecond'); - expect(str).toBe('0 milliseconds'); - }); - it('1 millisecond', () => { - const str = kbn.toDuration(1, 0, 'millisecond'); - expect(str).toBe('1 millisecond'); - }); - it('-1 millisecond', () => { - const str = kbn.toDuration(-1, 0, 'millisecond'); - expect(str).toBe('1 millisecond ago'); - }); - it('seconds', () => { - const str = kbn.toDuration(1, 0, 'second'); - expect(str).toBe('1 second'); - }); - it('minutes', () => { - const str = kbn.toDuration(1, 0, 'minute'); - expect(str).toBe('1 minute'); - }); - it('hours', () => { - const str = kbn.toDuration(1, 0, 'hour'); - expect(str).toBe('1 hour'); - }); - it('days', () => { - const str = kbn.toDuration(1, 0, 'day'); - expect(str).toBe('1 day'); - }); - it('weeks', () => { - const str = kbn.toDuration(1, 0, 'week'); - expect(str).toBe('1 week'); - }); - it('months', () => { - const str = kbn.toDuration(1, 0, 'month'); - expect(str).toBe('1 month'); - }); - it('years', () => { - const str = kbn.toDuration(1, 0, 'year'); - expect(str).toBe('1 year'); - }); - it('decimal days', () => { - const str = kbn.toDuration(1.5, 2, 'day'); - expect(str).toBe('1 day, 12 hours, 0 minutes'); - }); - it('decimal months', () => { - const str = kbn.toDuration(1.5, 3, 'month'); - expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); - }); - it('no decimals', () => { - const str = kbn.toDuration(38898367008, 0, 'millisecond'); - expect(str).toBe('1 year'); - }); - it('1 decimal', () => { - const str = kbn.toDuration(38898367008, 1, 'millisecond'); - expect(str).toBe('1 year, 2 months'); - }); - it('too many decimals', () => { - const str = kbn.toDuration(38898367008, 20, 'millisecond'); - expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); - }); - it('floating point error', () => { - const str = kbn.toDuration(36993906007, 8, 'millisecond'); - expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); - }); -}); - -describe('clock', () => { - it('null', () => { - const str = kbn.toClock(null, 0); - expect(str).toBe(''); - }); - it('size less than 1 second', () => { - const str = kbn.toClock(999, 0); - expect(str).toBe('999ms'); - }); - describe('size less than 1 minute', () => { - it('default', () => { - const str = kbn.toClock(59999); - expect(str).toBe('59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(59999, 0); - expect(str).toBe('59s'); - }); - }); - describe('size less than 1 hour', () => { - it('default', () => { - const str = kbn.toClock(3599999); - expect(str).toBe('59m:59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(3599999, 0); - expect(str).toBe('59m'); - }); - it('decimals equals 1', () => { - const str = kbn.toClock(3599999, 1); - expect(str).toBe('59m:59s'); - }); - }); - describe('size greater than or equal 1 hour', () => { - it('default', () => { - const str = kbn.toClock(7199999); - expect(str).toBe('01h:59m:59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(7199999, 0); - expect(str).toBe('01h'); - }); - it('decimals equals 1', () => { - const str = kbn.toClock(7199999, 1); - expect(str).toBe('01h:59m'); - }); - it('decimals equals 2', () => { - const str = kbn.toClock(7199999, 2); - expect(str).toBe('01h:59m:59s'); - }); - }); - describe('size greater than or equal 1 day', () => { - it('default', () => { - const str = kbn.toClock(89999999); - expect(str).toBe('24h:59m:59s:999ms'); - }); - it('decimals equals 0', () => { - const str = kbn.toClock(89999999, 0); - expect(str).toBe('24h'); - }); - it('decimals equals 1', () => { - const str = kbn.toClock(89999999, 1); - expect(str).toBe('24h:59m'); - }); - it('decimals equals 2', () => { - const str = kbn.toClock(89999999, 2); - expect(str).toBe('24h:59m:59s'); - }); - }); -}); - -describe('volume', () => { - it('1000m3', () => { - const str = kbn.valueFormats['m3'](1000, 1, null); - expect(str).toBe('1000.0 m³'); - }); -}); - -describe('hh:mm:ss', () => { - it('00:04:06', () => { - const str = kbn.valueFormats['dthms'](246, 1); - expect(str).toBe('00:04:06'); - }); - it('24:00:00', () => { - const str = kbn.valueFormats['dthms'](86400, 1); - expect(str).toBe('24:00:00'); - }); - it('6824413:53:20', () => { - const str = kbn.valueFormats['dthms'](24567890000, 1); - expect(str).toBe('6824413:53:20'); - }); -}); diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 34508e94a9f..6d73ab9fbd8 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -1,99 +1,5 @@ -import _ from 'lodash'; -import tinycolor from 'tinycolor2'; import config from 'app/core/config'; -export const PALETTE_ROWS = 4; -export const PALETTE_COLUMNS = 14; -export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; -export const OK_COLOR = 'rgba(11, 237, 50, 1)'; -export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; -export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; -export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; -export const REGION_FILL_ALPHA = 0.09; - -const colors = [ - '#7EB26D', // 0: pale green - '#EAB839', // 1: mustard - '#6ED0E0', // 2: light blue - '#EF843C', // 3: orange - '#E24D42', // 4: red - '#1F78C1', // 5: ocean - '#BA43A9', // 6: purple - '#705DA0', // 7: violet - '#508642', // 8: dark green - '#CCA300', // 9: dark sand - '#447EBC', - '#C15C17', - '#890F02', - '#0A437C', - '#6D1F62', - '#584477', - '#B7DBAB', - '#F4D598', - '#70DBED', - '#F9BA8F', - '#F29191', - '#82B5D8', - '#E5A8E2', - '#AEA2E0', - '#629E51', - '#E5AC0E', - '#64B0C8', - '#E0752D', - '#BF1B00', - '#0A50A1', - '#962D82', - '#614D93', - '#9AC48A', - '#F2C96D', - '#65C5DB', - '#F9934E', - '#EA6460', - '#5195CE', - '#D683CE', - '#806EB7', - '#3F6833', - '#967302', - '#2F575E', - '#99440A', - '#58140C', - '#052B51', - '#511749', - '#3F2B5B', - '#E0F9D7', - '#FCEACA', - '#CFFAFF', - '#F9E2D2', - '#FCE2DE', - '#BADFF4', - '#F9D9F9', - '#DEDAF7', -]; - -export function sortColorsByHue(hexColors) { - const hslColors = _.map(hexColors, hexToHsl); - - let sortedHSLColors = _.sortBy(hslColors, ['h']); - sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); - sortedHSLColors = _.map(sortedHSLColors, chunk => { - return _.sortBy(chunk, 'l'); - }); - sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors)); - - return _.map(sortedHSLColors, hslToHex); -} - -export function hexToHsl(color) { - return tinycolor(color).toHsl(); -} - -export function hslToHex(color) { - return tinycolor(color).toHexString(); -} - export function getThemeColor(dark: string, light: string): string { return config.bootData.user.lightTheme ? light : dark; } - -export let sortedColors = sortColorsByHue(colors); -export default colors; diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index bea166075dc..f3273ffa16d 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,9 +1,9 @@ import _ from 'lodash'; +import { colors } from '@grafana/ui'; import { renderUrl } from 'app/core/utils/url'; import kbn from 'app/core/utils/kbn'; import store from 'app/core/store'; -import colors from 'app/core/utils/colors'; import { parse as parseDate } from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; diff --git a/public/app/core/utils/factors.ts b/public/app/core/utils/factors.ts new file mode 100644 index 00000000000..e9ce327a631 --- /dev/null +++ b/public/app/core/utils/factors.ts @@ -0,0 +1,5 @@ +// Returns the factors of a number +// Example getFactors(12) -> [1, 2, 3, 4, 6, 12] +export default function getFactors(num: number): number[] { + return Array.from(new Array(num + 1), (_, i) => i).filter(i => num % i === 0); +} diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index d32844c44ed..a3a96f8afc3 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import moment from 'moment'; +import { getValueFormat, getValueFormatterIndex, getValueFormats } from '@grafana/ui'; const kbn: any = {}; @@ -280,942 +280,33 @@ kbn.roundValue = (num, decimals) => { return Math.round(parseFloat(formatted)) / n; }; -///// FORMAT FUNCTION CONSTRUCTORS ///// - -kbn.formatBuilders = {}; - -// Formatter which always appends a fixed unit string to the value. No -// scaling of the value is performed. -kbn.formatBuilders.fixedUnit = unit => { - return (size, decimals) => { - if (size === null) { - return ''; - } - return kbn.toFixed(size, decimals) + ' ' + unit; - }; -}; - -// Formatter which scales the unit string geometrically according to the given -// numeric factor. Repeatedly scales the value down by the factor until it is -// less than the factor in magnitude, or the end of the array is reached. -kbn.formatBuilders.scaledUnits = (factor, extArray) => { - return (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - let steps = 0; - const limit = extArray.length; - - while (Math.abs(size) >= factor) { - steps++; - size /= factor; - - if (steps >= limit) { - return 'NA'; - } - } - - if (steps > 0 && scaledDecimals !== null) { - decimals = scaledDecimals + 3 * steps; - } - - return kbn.toFixed(size, decimals) + extArray[steps]; - }; -}; - -// Extension of the scaledUnits builder which uses SI decimal prefixes. If an -// offset is given, it adjusts the starting units at the given prefix; a value -// of 0 starts at no scale; -3 drops to nano, +2 starts at mega, etc. -kbn.formatBuilders.decimalSIPrefix = (unit, offset) => { - let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; - prefixes = prefixes.slice(3 + (offset || 0)); - const units = prefixes.map(p => { - return ' ' + p + unit; - }); - return kbn.formatBuilders.scaledUnits(1000, units); -}; - -// Extension of the scaledUnits builder which uses SI binary prefixes. If -// offset is given, it starts the units at the given prefix; otherwise, the -// offset defaults to zero and the initial unit is not prefixed. -kbn.formatBuilders.binarySIPrefix = (unit, offset) => { - const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); - const units = prefixes.map(p => { - return ' ' + p + unit; - }); - return kbn.formatBuilders.scaledUnits(1024, units); -}; - -// Currency formatter for prefixing a symbol onto a number. Supports scaling -// up to the trillions. -kbn.formatBuilders.currency = symbol => { - const units = ['', 'K', 'M', 'B', 'T']; - const scaler = kbn.formatBuilders.scaledUnits(1000, units); - return (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - const scaled = scaler(size, decimals, scaledDecimals); - return symbol + scaled; - }; -}; - -kbn.formatBuilders.simpleCountUnit = symbol => { - const units = ['', 'K', 'M', 'B', 'T']; - const scaler = kbn.formatBuilders.scaledUnits(1000, units); - return (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - const scaled = scaler(size, decimals, scaledDecimals); - return scaled + ' ' + symbol; - }; -}; - -///// VALUE FORMATS ///// - -// Dimensionless Units -kbn.valueFormats.none = kbn.toFixed; -kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, [ - '', - ' K', - ' Mil', - ' Bil', - ' Tri', - ' Quadr', - ' Quint', - ' Sext', - ' Sept', -]); -kbn.valueFormats.dB = kbn.formatBuilders.fixedUnit('dB'); - -kbn.valueFormats.percent = (size, decimals) => { - if (size === null) { - return ''; - } - return kbn.toFixed(size, decimals) + '%'; -}; - -kbn.valueFormats.percentunit = (size, decimals) => { - if (size === null) { - return ''; - } - return kbn.toFixed(100 * size, decimals) + '%'; -}; - -/* Formats the value to hex. Uses float if specified decimals are not 0. - * There are two submenu - * , one with 0x, and one without */ - -kbn.valueFormats.hex = (value, decimals) => { - if (value == null) { - return ''; - } - return parseFloat(kbn.toFixed(value, decimals)) - .toString(16) - .toUpperCase(); -}; - -kbn.valueFormats.hex0x = (value, decimals) => { - if (value == null) { - return ''; - } - const hexString = kbn.valueFormats.hex(value, decimals); - if (hexString.substring(0, 1) === '-') { - return '-0x' + hexString.substring(1); - } - return '0x' + hexString; -}; - -kbn.valueFormats.sci = (value, decimals) => { - if (value == null) { - return ''; - } - return value.toExponential(decimals); -}; - -kbn.valueFormats.locale = (value, decimals) => { - if (value == null) { - return ''; - } - return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); -}; - -// Currencies -kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$'); -kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£'); -kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€'); -kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥'); -kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽'); -kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴'); -kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$'); -kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr'); -kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk'); -kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF'); -kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł'); -kbn.valueFormats.currencyBTC = kbn.formatBuilders.currency('฿'); - -// Data (Binary) -kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); -kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B'); -kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1); -kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2); -kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3); - -// Data (Decimal) -kbn.valueFormats.decbits = kbn.formatBuilders.decimalSIPrefix('b'); -kbn.valueFormats.decbytes = kbn.formatBuilders.decimalSIPrefix('B'); -kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1); -kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2); -kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3); - -// Data Rate -kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps'); -kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); -kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('B/s'); -kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); -kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1); -kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); -kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); -kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); -kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); - -// Floating Point Operations per Second -kbn.valueFormats.flops = kbn.formatBuilders.decimalSIPrefix('FLOP/s'); -kbn.valueFormats.mflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 2); -kbn.valueFormats.gflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 3); -kbn.valueFormats.tflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 4); -kbn.valueFormats.pflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 5); -kbn.valueFormats.eflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 6); - -// Hash Rate -kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s'); -kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1); -kbn.valueFormats.MHs = kbn.formatBuilders.decimalSIPrefix('H/s', 2); -kbn.valueFormats.GHs = kbn.formatBuilders.decimalSIPrefix('H/s', 3); -kbn.valueFormats.THs = kbn.formatBuilders.decimalSIPrefix('H/s', 4); -kbn.valueFormats.PHs = kbn.formatBuilders.decimalSIPrefix('H/s', 5); -kbn.valueFormats.EHs = kbn.formatBuilders.decimalSIPrefix('H/s', 6); - -// Throughput -kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); -kbn.valueFormats.reqps = kbn.formatBuilders.simpleCountUnit('reqps'); -kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); -kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps'); -kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops'); -kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm'); -kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm'); -kbn.valueFormats.wpm = kbn.formatBuilders.simpleCountUnit('wpm'); - -// Energy -kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W'); -kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1); -kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1); -kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1); -kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m²'); -kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); -kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); -kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); -kbn.valueFormats.kvoltampreact = kbn.formatBuilders.decimalSIPrefix('var', 1); -kbn.valueFormats.watth = kbn.formatBuilders.decimalSIPrefix('Wh'); -kbn.valueFormats.kwatth = kbn.formatBuilders.decimalSIPrefix('Wh', 1); -kbn.valueFormats.joule = kbn.formatBuilders.decimalSIPrefix('J'); -kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV'); -kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A'); -kbn.valueFormats.kamp = kbn.formatBuilders.decimalSIPrefix('A', 1); -kbn.valueFormats.mamp = kbn.formatBuilders.decimalSIPrefix('A', -1); -kbn.valueFormats.volt = kbn.formatBuilders.decimalSIPrefix('V'); -kbn.valueFormats.kvolt = kbn.formatBuilders.decimalSIPrefix('V', 1); -kbn.valueFormats.mvolt = kbn.formatBuilders.decimalSIPrefix('V', -1); -kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm'); -kbn.valueFormats.ohm = kbn.formatBuilders.decimalSIPrefix('Ω'); -kbn.valueFormats.lumens = kbn.formatBuilders.decimalSIPrefix('Lm'); - -// Temperature -kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C'); -kbn.valueFormats.farenheit = kbn.formatBuilders.fixedUnit('°F'); -kbn.valueFormats.kelvin = kbn.formatBuilders.fixedUnit('K'); -kbn.valueFormats.humidity = kbn.formatBuilders.fixedUnit('%H'); - -// Pressure -kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar'); -kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); -kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); -kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); -kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa'); -kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); -kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); - -// Force -kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm'); -kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1); -kbn.valueFormats.forceN = kbn.formatBuilders.decimalSIPrefix('N'); -kbn.valueFormats.forcekN = kbn.formatBuilders.decimalSIPrefix('N', 1); - -// Length -kbn.valueFormats.lengthm = kbn.formatBuilders.decimalSIPrefix('m'); -kbn.valueFormats.lengthmm = kbn.formatBuilders.decimalSIPrefix('m', -1); -kbn.valueFormats.lengthkm = kbn.formatBuilders.decimalSIPrefix('m', 1); -kbn.valueFormats.lengthmi = kbn.formatBuilders.fixedUnit('mi'); -kbn.valueFormats.lengthft = kbn.formatBuilders.fixedUnit('ft'); - -// Area -kbn.valueFormats.areaM2 = kbn.formatBuilders.fixedUnit('m²'); -kbn.valueFormats.areaF2 = kbn.formatBuilders.fixedUnit('ft²'); -kbn.valueFormats.areaMI2 = kbn.formatBuilders.fixedUnit('mi²'); - -// Mass -kbn.valueFormats.massmg = kbn.formatBuilders.decimalSIPrefix('g', -1); -kbn.valueFormats.massg = kbn.formatBuilders.decimalSIPrefix('g'); -kbn.valueFormats.masskg = kbn.formatBuilders.decimalSIPrefix('g', 1); -kbn.valueFormats.masst = kbn.formatBuilders.fixedUnit('t'); - -// Velocity -kbn.valueFormats.velocityms = kbn.formatBuilders.fixedUnit('m/s'); -kbn.valueFormats.velocitykmh = kbn.formatBuilders.fixedUnit('km/h'); -kbn.valueFormats.velocitymph = kbn.formatBuilders.fixedUnit('mph'); -kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn'); - -// Acceleration -kbn.valueFormats.accMS2 = kbn.formatBuilders.fixedUnit('m/sec²'); -kbn.valueFormats.accFS2 = kbn.formatBuilders.fixedUnit('f/sec²'); -kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g'); - -// Volume -kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); -kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1); -kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m³'); -kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm³'); -kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm³'); -kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); - -// Flow -kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm'); -kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); -kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); -kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); -kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h'); -kbn.valueFormats.flowlpm = kbn.formatBuilders.fixedUnit('l/min'); -kbn.valueFormats.flowmlpm = kbn.formatBuilders.fixedUnit('mL/min'); - -// Angle -kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); -kbn.valueFormats.radian = kbn.formatBuilders.fixedUnit('rad'); -kbn.valueFormats.grad = kbn.formatBuilders.fixedUnit('grad'); - -// Radiation -kbn.valueFormats.radbq = kbn.formatBuilders.decimalSIPrefix('Bq'); -kbn.valueFormats.radci = kbn.formatBuilders.decimalSIPrefix('Ci'); -kbn.valueFormats.radgy = kbn.formatBuilders.decimalSIPrefix('Gy'); -kbn.valueFormats.radrad = kbn.formatBuilders.decimalSIPrefix('rad'); -kbn.valueFormats.radsv = kbn.formatBuilders.decimalSIPrefix('Sv'); -kbn.valueFormats.radrem = kbn.formatBuilders.decimalSIPrefix('rem'); -kbn.valueFormats.radexpckg = kbn.formatBuilders.decimalSIPrefix('C/kg'); -kbn.valueFormats.radr = kbn.formatBuilders.decimalSIPrefix('R'); -kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h'); - -// Concentration -kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); -kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb'); -kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m³'); -kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm³'); -kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m³'); -kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm³'); -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'); - -kbn.valueFormats.ms = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + ' ms'; - } else if (Math.abs(size) < 60000) { - // Less than 1 min - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); - } else if (Math.abs(size) < 3600000) { - // Less than 1 hour, divide in minutes - return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); - } else if (Math.abs(size) < 86400000) { - // Less than one day, divide in hours - return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); - } else if (Math.abs(size) < 31536000000) { - // Less than one year, divide in days - return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); - } - - return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); -}; - -kbn.valueFormats.s = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - // Less than 1 µs, divide in ns - if (Math.abs(size) < 0.000001) { - return kbn.toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); - } - // Less than 1 ms, divide in µs - if (Math.abs(size) < 0.001) { - return kbn.toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); - } - // Less than 1 second, divide in ms - if (Math.abs(size) < 1) { - return kbn.toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); - } - - if (Math.abs(size) < 60) { - return kbn.toFixed(size, decimals) + ' s'; - } else if (Math.abs(size) < 3600) { - // Less than 1 hour, divide in minutes - return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); - } else if (Math.abs(size) < 86400) { - // Less than one day, divide in hours - return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour'); - } else if (Math.abs(size) < 604800) { - // Less than one week, divide in days - return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); - } else if (Math.abs(size) < 31536000) { - // Less than one year, divide in week - return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); - } - - return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); -}; - -kbn.valueFormats['µs'] = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + ' µs'; - } else if (Math.abs(size) < 1000000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); - } else { - return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s'); - } -}; - -kbn.valueFormats.ns = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + ' ns'; - } else if (Math.abs(size) < 1000000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); - } else if (Math.abs(size) < 1000000000) { - return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); - } else if (Math.abs(size) < 60000000000) { - return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); - } else { - return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); - } -}; - -kbn.valueFormats.m = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 60) { - return kbn.toFixed(size, decimals) + ' min'; - } else if (Math.abs(size) < 1440) { - return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); - } else if (Math.abs(size) < 10080) { - return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); - } else if (Math.abs(size) < 604800) { - return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); - } else { - return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); - } -}; - -kbn.valueFormats.h = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 24) { - return kbn.toFixed(size, decimals) + ' hour'; - } else if (Math.abs(size) < 168) { - return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); - } else if (Math.abs(size) < 8760) { - return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week'); - } else { - return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year'); - } -}; - -kbn.valueFormats.d = (size, decimals, scaledDecimals) => { - if (size === null) { - return ''; - } - - if (Math.abs(size) < 7) { - return kbn.toFixed(size, decimals) + ' day'; - } else if (Math.abs(size) < 365) { - return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); - } else { - return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year'); - } -}; - -kbn.toDuration = (size, decimals, timeScale) => { - if (size === null) { - return ''; - } - if (size === 0) { - return '0 ' + timeScale + 's'; - } - if (size < 0) { - return kbn.toDuration(-size, decimals, timeScale) + ' ago'; - } - - const units = [ - { short: 'y', long: 'year' }, - { short: 'M', long: 'month' }, - { short: 'w', long: 'week' }, - { short: 'd', long: 'day' }, - { short: 'h', long: 'hour' }, - { short: 'm', long: 'minute' }, - { short: 's', long: 'second' }, - { short: 'ms', long: 'millisecond' }, - ]; - // convert $size to milliseconds - // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors - size *= - kbn.intervals_in_seconds[ - units.find(e => { - return e.long === timeScale; - }).short - ] * 1000; - - const strings = []; - // after first value >= 1 print only $decimals more - let decrementDecimals = false; - for (let i = 0; i < units.length && decimals >= 0; i++) { - const interval = kbn.intervals_in_seconds[units[i].short] * 1000; - const value = size / interval; - if (value >= 1 || decrementDecimals) { - decrementDecimals = true; - const floor = Math.floor(value); - const unit = units[i].long + (floor !== 1 ? 's' : ''); - strings.push(floor + ' ' + unit); - size = size % interval; - decimals--; - } - } - - return strings.join(', '); -}; - -kbn.toClock = (size, decimals) => { - if (size === null) { - return ''; - } - - // < 1 second - if (size < 1000) { - return moment.utc(size).format('SSS\\m\\s'); - } - - // < 1 minute - if (size < 60000) { - let format = 'ss\\s:SSS\\m\\s'; - if (decimals === 0) { - format = 'ss\\s'; - } - return moment.utc(size).format(format); - } - - // < 1 hour - if (size < 3600000) { - let format = 'mm\\m:ss\\s:SSS\\m\\s'; - if (decimals === 0) { - format = 'mm\\m'; - } else if (decimals === 1) { - format = 'mm\\m:ss\\s'; - } - return moment.utc(size).format(format); - } - - let format = 'mm\\m:ss\\s:SSS\\m\\s'; - - const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`; - - if (decimals === 0) { - format = ''; - } else if (decimals === 1) { - format = 'mm\\m'; - } else if (decimals === 2) { - format = 'mm\\m:ss\\s'; - } - - return format ? `${hours}:${moment.utc(size).format(format)}` : hours; -}; - -kbn.valueFormats.dtdurationms = (size, decimals) => { - return kbn.toDuration(size, decimals, 'millisecond'); -}; - -kbn.valueFormats.dtdurations = (size, decimals) => { - return kbn.toDuration(size, decimals, 'second'); -}; - -kbn.valueFormats.dthms = (size, decimals) => { - return kbn.secondsToHhmmss(size); -}; - -kbn.valueFormats.timeticks = (size, decimals, scaledDecimals) => { - return kbn.valueFormats.s(size / 100, decimals, scaledDecimals); -}; - -kbn.valueFormats.clockms = (size, decimals) => { - return kbn.toClock(size, decimals); -}; - -kbn.valueFormats.clocks = (size, decimals) => { - return kbn.toClock(size * 1000, decimals); -}; - -kbn.valueFormats.dateTimeAsIso = (epoch, isUtc) => { - const time = isUtc ? moment.utc(epoch) : moment(epoch); - - if (moment().isSame(epoch, 'day')) { - return time.format('HH:mm:ss'); - } - return time.format('YYYY-MM-DD HH:mm:ss'); -}; - -kbn.valueFormats.dateTimeAsUS = (epoch, isUtc) => { - const time = isUtc ? moment.utc(epoch) : moment(epoch); - - if (moment().isSame(epoch, 'day')) { - return time.format('h:mm:ss a'); - } - return time.format('MM/DD/YYYY h:mm:ss a'); -}; - -kbn.valueFormats.dateTimeFromNow = (epoch, isUtc) => { - const time = isUtc ? moment.utc(epoch) : moment(epoch); - return time.fromNow(); -}; - ///// FORMAT MENU ///// kbn.getUnitFormats = () => { - return [ - { - text: 'none', - submenu: [ - { text: 'none', value: 'none' }, - { text: 'short', value: 'short' }, - { text: 'percent (0-100)', value: 'percent' }, - { text: 'percent (0.0-1.0)', value: 'percentunit' }, - { text: 'Humidity (%H)', value: 'humidity' }, - { text: 'decibel', value: 'dB' }, - { text: 'hexadecimal (0x)', value: 'hex0x' }, - { text: 'hexadecimal', value: 'hex' }, - { text: 'scientific notation', value: 'sci' }, - { text: 'locale format', value: 'locale' }, - ], - }, - { - text: 'currency', - submenu: [ - { text: 'Dollars ($)', value: 'currencyUSD' }, - { text: 'Pounds (£)', value: 'currencyGBP' }, - { text: 'Euro (€)', value: 'currencyEUR' }, - { text: 'Yen (¥)', value: 'currencyJPY' }, - { text: 'Rubles (₽)', value: 'currencyRUB' }, - { text: 'Hryvnias (₴)', value: 'currencyUAH' }, - { text: 'Real (R$)', value: 'currencyBRL' }, - { text: 'Danish Krone (kr)', value: 'currencyDKK' }, - { text: 'Icelandic Króna (kr)', value: 'currencyISK' }, - { text: 'Norwegian Krone (kr)', value: 'currencyNOK' }, - { text: 'Swedish Krona (kr)', value: 'currencySEK' }, - { text: 'Czech koruna (czk)', value: 'currencyCZK' }, - { text: 'Swiss franc (CHF)', value: 'currencyCHF' }, - { text: 'Polish Złoty (PLN)', value: 'currencyPLN' }, - { text: 'Bitcoin (฿)', value: 'currencyBTC' }, - ], - }, - { - text: 'time', - submenu: [ - { text: 'Hertz (1/s)', value: 'hertz' }, - { text: 'nanoseconds (ns)', value: 'ns' }, - { text: 'microseconds (µs)', value: 'µs' }, - { text: 'milliseconds (ms)', value: 'ms' }, - { text: 'seconds (s)', value: 's' }, - { text: 'minutes (m)', value: 'm' }, - { text: 'hours (h)', value: 'h' }, - { text: 'days (d)', value: 'd' }, - { text: 'duration (ms)', value: 'dtdurationms' }, - { text: 'duration (s)', value: 'dtdurations' }, - { text: 'duration (hh:mm:ss)', value: 'dthms' }, - { text: 'Timeticks (s/100)', value: 'timeticks' }, - { text: 'clock (ms)', value: 'clockms' }, - { text: 'clock (s)', value: 'clocks' }, - ], - }, - { - text: 'date & time', - submenu: [ - { text: 'YYYY-MM-DD HH:mm:ss', value: 'dateTimeAsIso' }, - { text: 'DD/MM/YYYY h:mm:ss a', value: 'dateTimeAsUS' }, - { text: 'From Now', value: 'dateTimeFromNow' }, - ], - }, - { - text: 'data (IEC)', - submenu: [ - { text: 'bits', value: 'bits' }, - { text: 'bytes', value: 'bytes' }, - { text: 'kibibytes', value: 'kbytes' }, - { text: 'mebibytes', value: 'mbytes' }, - { text: 'gibibytes', value: 'gbytes' }, - ], - }, - { - text: 'data (Metric)', - submenu: [ - { text: 'bits', value: 'decbits' }, - { text: 'bytes', value: 'decbytes' }, - { text: 'kilobytes', value: 'deckbytes' }, - { text: 'megabytes', value: 'decmbytes' }, - { text: 'gigabytes', value: 'decgbytes' }, - ], - }, - { - text: 'data rate', - submenu: [ - { text: 'packets/sec', value: 'pps' }, - { text: 'bits/sec', value: 'bps' }, - { text: 'bytes/sec', value: 'Bps' }, - { text: 'kilobits/sec', value: 'Kbits' }, - { text: 'kilobytes/sec', value: 'KBs' }, - { text: 'megabits/sec', value: 'Mbits' }, - { text: 'megabytes/sec', value: 'MBs' }, - { text: 'gigabytes/sec', value: 'GBs' }, - { text: 'gigabits/sec', value: 'Gbits' }, - ], - }, - { - text: 'hash rate', - submenu: [ - { text: 'hashes/sec', value: 'Hs' }, - { text: 'kilohashes/sec', value: 'KHs' }, - { text: 'megahashes/sec', value: 'MHs' }, - { text: 'gigahashes/sec', value: 'GHs' }, - { text: 'terahashes/sec', value: 'THs' }, - { text: 'petahashes/sec', value: 'PHs' }, - { text: 'exahashes/sec', value: 'EHs' }, - ], - }, - { - text: 'computation throughput', - submenu: [ - { text: 'FLOP/s', value: 'flops' }, - { text: 'MFLOP/s', value: 'mflops' }, - { text: 'GFLOP/s', value: 'gflops' }, - { text: 'TFLOP/s', value: 'tflops' }, - { text: 'PFLOP/s', value: 'pflops' }, - { text: 'EFLOP/s', value: 'eflops' }, - ], - }, - { - text: 'throughput', - submenu: [ - { text: 'ops/sec (ops)', value: 'ops' }, - { text: 'requests/sec (rps)', value: 'reqps' }, - { text: 'reads/sec (rps)', value: 'rps' }, - { text: 'writes/sec (wps)', value: 'wps' }, - { text: 'I/O ops/sec (iops)', value: 'iops' }, - { text: 'ops/min (opm)', value: 'opm' }, - { text: 'reads/min (rpm)', value: 'rpm' }, - { text: 'writes/min (wpm)', value: 'wpm' }, - ], - }, - { - text: 'length', - submenu: [ - { text: 'millimetre (mm)', value: 'lengthmm' }, - { text: 'meter (m)', value: 'lengthm' }, - { text: 'feet (ft)', value: 'lengthft' }, - { text: 'kilometer (km)', value: 'lengthkm' }, - { text: 'mile (mi)', value: 'lengthmi' }, - ], - }, - { - text: 'area', - submenu: [ - { text: 'Square Meters (m²)', value: 'areaM2' }, - { text: 'Square Feet (ft²)', value: 'areaF2' }, - { text: 'Square Miles (mi²)', value: 'areaMI2' }, - ], - }, - { - text: 'mass', - submenu: [ - { text: 'milligram (mg)', value: 'massmg' }, - { text: 'gram (g)', value: 'massg' }, - { text: 'kilogram (kg)', value: 'masskg' }, - { text: 'metric ton (t)', value: 'masst' }, - ], - }, - { - text: 'velocity', - submenu: [ - { text: 'metres/second (m/s)', value: 'velocityms' }, - { text: 'kilometers/hour (km/h)', value: 'velocitykmh' }, - { text: 'miles/hour (mph)', value: 'velocitymph' }, - { text: 'knot (kn)', value: 'velocityknot' }, - ], - }, - { - text: 'volume', - submenu: [ - { text: 'millilitre (mL)', value: 'mlitre' }, - { text: 'litre (L)', value: 'litre' }, - { text: 'cubic metre', value: 'm3' }, - { text: 'Normal cubic metre', value: 'Nm3' }, - { text: 'cubic decimetre', value: 'dm3' }, - { text: 'gallons', value: 'gallons' }, - ], - }, - { - text: 'energy', - submenu: [ - { text: 'Watt (W)', value: 'watt' }, - { text: 'Kilowatt (kW)', value: 'kwatt' }, - { text: 'Milliwatt (mW)', value: 'mwatt' }, - { text: 'Watt per square meter (W/m²)', value: 'Wm2' }, - { text: 'Volt-ampere (VA)', value: 'voltamp' }, - { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, - { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, - { text: 'Kilovolt-ampere reactive (kvar)', value: 'kvoltampreact' }, - { text: 'Watt-hour (Wh)', value: 'watth' }, - { text: 'Kilowatt-hour (kWh)', value: 'kwatth' }, - { text: 'Kilowatt-min (kWm)', value: 'kwattm' }, - { text: 'Joule (J)', value: 'joule' }, - { text: 'Electron volt (eV)', value: 'ev' }, - { text: 'Ampere (A)', value: 'amp' }, - { text: 'Kiloampere (kA)', value: 'kamp' }, - { text: 'Milliampere (mA)', value: 'mamp' }, - { text: 'Volt (V)', value: 'volt' }, - { text: 'Kilovolt (kV)', value: 'kvolt' }, - { text: 'Millivolt (mV)', value: 'mvolt' }, - { text: 'Decibel-milliwatt (dBm)', value: 'dBm' }, - { text: 'Ohm (Ω)', value: 'ohm' }, - { text: 'Lumens (Lm)', value: 'lumens' }, - ], - }, - { - text: 'temperature', - submenu: [ - { text: 'Celsius (°C)', value: 'celsius' }, - { text: 'Farenheit (°F)', value: 'farenheit' }, - { text: 'Kelvin (K)', value: 'kelvin' }, - ], - }, - { - text: 'pressure', - submenu: [ - { text: 'Millibars', value: 'pressurembar' }, - { text: 'Bars', value: 'pressurebar' }, - { text: 'Kilobars', value: 'pressurekbar' }, - { text: 'Hectopascals', value: 'pressurehpa' }, - { text: 'Kilopascals', value: 'pressurekpa' }, - { text: 'Inches of mercury', value: 'pressurehg' }, - { text: 'PSI', value: 'pressurepsi' }, - ], - }, - { - text: 'force', - submenu: [ - { text: 'Newton-meters (Nm)', value: 'forceNm' }, - { text: 'Kilonewton-meters (kNm)', value: 'forcekNm' }, - { text: 'Newtons (N)', value: 'forceN' }, - { text: 'Kilonewtons (kN)', value: 'forcekN' }, - ], - }, - { - text: 'flow', - submenu: [ - { text: 'Gallons/min (gpm)', value: 'flowgpm' }, - { text: 'Cubic meters/sec (cms)', value: 'flowcms' }, - { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, - { text: 'Cubic feet/min (cfm)', value: 'flowcfm' }, - { text: 'Litre/hour', value: 'litreh' }, - { text: 'Litre/min (l/min)', value: 'flowlpm' }, - { text: 'milliLitre/min (mL/min)', value: 'flowmlpm' }, - ], - }, - { - text: 'angle', - submenu: [ - { text: 'Degrees (°)', value: 'degree' }, - { text: 'Radians', value: 'radian' }, - { text: 'Gradian', value: 'grad' }, - ], - }, - { - text: 'acceleration', - submenu: [ - { text: 'Meters/sec²', value: 'accMS2' }, - { text: 'Feet/sec²', value: 'accFS2' }, - { text: 'G unit', value: 'accG' }, - ], - }, - { - text: 'radiation', - submenu: [ - { text: 'Becquerel (Bq)', value: 'radbq' }, - { text: 'curie (Ci)', value: 'radci' }, - { text: 'Gray (Gy)', value: 'radgy' }, - { text: 'rad', value: 'radrad' }, - { text: 'Sievert (Sv)', value: 'radsv' }, - { text: 'rem', value: 'radrem' }, - { text: 'Exposure (C/kg)', value: 'radexpckg' }, - { text: 'roentgen (R)', value: 'radr' }, - { text: 'Sievert/hour (Sv/h)', value: 'radsvh' }, - ], - }, - { - text: 'concentration', - submenu: [ - { text: 'parts-per-million (ppm)', value: 'ppm' }, - { text: 'parts-per-billion (ppb)', value: 'conppb' }, - { text: 'nanogram per cubic meter (ng/m³)', value: 'conngm3' }, - { text: 'nanogram per normal cubic meter (ng/Nm³)', value: 'conngNm3' }, - { text: 'microgram per cubic meter (μg/m³)', value: 'conμgm3' }, - { text: 'microgram per normal cubic meter (μg/Nm³)', value: 'conμgNm3' }, - { text: 'milligram per cubic meter (mg/m³)', value: 'conmgm3' }, - { 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' }, - ], - }, - ]; + return getValueFormats(); }; +// +// Backward compatible layer for value formats to support old plugins +// +if (typeof Proxy !== "undefined") { + kbn.valueFormats = new Proxy(kbn.valueFormats, { + get(target, name, receiver) { + if (typeof name !== 'string') { + throw {message: `Value format ${String(name)} is not a string` }; + } + + const formatter = getValueFormat(name); + if (formatter) { + return formatter; + } + + // default to look here + return Reflect.get(target, name, receiver); + } + }); +} else { + kbn.valueFormats = getValueFormatterIndex(); +} + export default kbn; diff --git a/public/app/features/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx index a5afbc198fc..2a1b3d12ecf 100644 --- a/public/app/features/alerting/AlertTab.tsx +++ b/public/app/features/alerting/AlertTab.tsx @@ -14,6 +14,7 @@ import 'app/features/alerting/AlertTabCtrl'; // Types import { DashboardModel } from '../dashboard/dashboard_model'; import { PanelModel } from '../dashboard/panel_model'; +import { TestRuleResult } from './TestRuleResult'; interface Props { angularPanel?: AngularComponent; @@ -65,9 +66,7 @@ export class AlertTab extends PureComponent { const loader = getAngularLoader(); const template = ''; - const scopeProps = { - ctrl: this.panelCtrl, - }; + const scopeProps = { ctrl: this.panelCtrl }; this.component = loader.load(this.element, scopeProps, template); } @@ -111,6 +110,16 @@ export class AlertTab extends PureComponent { }; }; + renderTestRuleResult = () => { + const { panel, dashboard } = this.props; + return ; + }; + + testRule = (): EditorToolbarView => ({ + title: 'Test Rule', + render: () => this.renderTestRuleResult(), + }); + onAddAlert = () => { this.panelCtrl._enableAlert(); this.component.digest(); @@ -120,7 +129,7 @@ export class AlertTab extends PureComponent { render() { const { alert } = this.props.panel; - const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : []; + const toolbarItems = alert ? [this.stateHistory(), this.testRule(), this.deleteAlert()] : []; const model = { title: 'Panel has no alert rule defined', diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 2be25e9df6a..af00e79b085 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -9,8 +9,6 @@ import appEvents from 'app/core/app_events'; export class AlertTabCtrl { panel: any; panelCtrl: any; - testing: boolean; - testResult: any; subTabIndex: number; conditionTypes: any; alert: any; @@ -406,21 +404,6 @@ export class AlertTabCtrl { }, }); } - - test() { - this.testing = true; - this.testResult = false; - - const payload = { - dashboard: this.dashboardSrv.getCurrent().getSaveModelClone(), - panelId: this.panelCtrl.panel.id, - }; - - return this.backendSrv.post('/api/alerts/test', payload).then(res => { - this.testResult = res; - this.testing = false; - }); - } } /** @ngInject */ diff --git a/public/app/features/alerting/TestRuleResult.test.tsx b/public/app/features/alerting/TestRuleResult.test.tsx new file mode 100644 index 00000000000..9beb5ade632 --- /dev/null +++ b/public/app/features/alerting/TestRuleResult.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import { Props, TestRuleResult } from './TestRuleResult'; + +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => ({ + post: jest.fn(), + }), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + panelId: 1, + dashboard: new DashboardModel({ panels: [{ id: 1 }] }), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + + return { wrapper, instance: wrapper.instance() as TestRuleResult }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + describe('component did mount', () => { + it('should call testRule', () => { + const { instance } = setup(); + instance.testRule = jest.fn(); + instance.componentDidMount(); + + expect(instance.testRule).toHaveBeenCalled(); + }); + }); +}); diff --git a/public/app/features/alerting/TestRuleResult.tsx b/public/app/features/alerting/TestRuleResult.tsx new file mode 100644 index 00000000000..4014e529597 --- /dev/null +++ b/public/app/features/alerting/TestRuleResult.tsx @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react'; +import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardModel } from '../dashboard/dashboard_model'; +import { LoadingPlaceholder } from '@grafana/ui/src'; + +export interface Props { + panelId: number; + dashboard: DashboardModel; +} + +interface State { + isLoading: boolean; + testRuleResponse: {}; +} + +export class TestRuleResult extends PureComponent { + readonly state: State = { + isLoading: false, + testRuleResponse: {}, + }; + + componentDidMount() { + this.testRule(); + } + + async testRule() { + const { panelId, dashboard } = this.props; + const payload = { dashboard: dashboard.getSaveModelClone(), panelId }; + + this.setState({ isLoading: true }); + const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload); + this.setState({ isLoading: false, testRuleResponse }); + } + + render() { + const { testRuleResponse, isLoading } = this.state; + + if (isLoading === true) { + return ; + } + + return ; + } +} diff --git a/public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap b/public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap new file mode 100644 index 00000000000..73f85f12354 --- /dev/null +++ b/public/app/features/alerting/__snapshots__/TestRuleResult.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` + +`; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index da862203da6..9dfd3da47f9 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -2,8 +2,8 @@
{{ctrl.error}}
-
-
+
+

Rule

@@ -121,27 +121,13 @@
- -
- -
-
- -
- Evaluating rule -
- -
-
-
-
Notifications
-
+
+
Notifications
+
Send to diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index db748e639a1..6966d3cdc82 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -1,8 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; import tinycolor from 'tinycolor2'; -import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { AnnotationEvent } from './event'; import { OK_COLOR, ALERTING_COLOR, @@ -10,7 +8,10 @@ import { PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, -} from 'app/core/utils/colors'; +} from '@grafana/ui'; + +import { MetricsPanelCtrl } from 'app/plugins/sdk'; +import { AnnotationEvent } from './event'; export class EventManager { event: AnnotationEvent; diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index abd12ab4b13..2dbeb6c6e80 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -9,6 +9,7 @@ import { } from 'app/core/constants'; import { PanelModel } from './panel_model'; import { DashboardModel } from './dashboard_model'; +import getFactors from 'app/core/utils/factors'; export class DashboardMigrator { dashboard: DashboardModel; @@ -21,7 +22,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades = []; - this.dashboard.schemaVersion = 16; + this.dashboard.schemaVersion = 17; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -368,6 +369,24 @@ export class DashboardMigrator { this.upgradeToGridLayout(old); } + if (oldVersion < 17) { + panelUpgrades.push(panel => { + if (panel.minSpan) { + const max = GRID_COLUMN_COUNT / panel.minSpan; + const factors = getFactors(GRID_COLUMN_COUNT); + // find the best match compared to factors + // (ie. [1,2,3,4,6,12,24] for 24 columns) + panel.maxPerRow = + factors[ + _.findIndex(factors, o => { + return o > max; + }) - 1 + ]; + } + delete panel.minSpan; + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 6f98bc5a17a..2ae2df0124b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -1,8 +1,8 @@ import moment from 'moment'; import _ from 'lodash'; +import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; -import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors'; import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; import sortByKeys from 'app/core/utils/sort_by_keys'; @@ -442,7 +442,7 @@ export class DashboardModel { } const selectedOptions = this.getSelectedVariableOptions(variable); - const minWidth = panel.minSpan || 6; + const maxPerRow = panel.maxPerRow || 4; let xPos = 0; let yPos = panel.gridPos.y; @@ -462,7 +462,7 @@ export class DashboardModel { } else { // set width based on how many are selected // assumed the repeated panels should take up full row width - copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, GRID_COLUMN_COUNT / maxPerRow); copy.gridPos.x = xPos; copy.gridPos.y = yPos; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index fa21276d7a2..d71a274ab10 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,6 +1,8 @@ // Library import React, { Component } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; + import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services @@ -12,7 +14,6 @@ import kbn from 'app/core/utils/kbn'; // Types import { DataQueryOptions, DataQueryResponse } from 'app/types'; import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui'; -import { Themes } from 'app/core/components/Tooltip/Popper'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; @@ -144,10 +145,10 @@ export class DataPanel extends Component { this.setState({ loading: LoadingState.Error, isFirstLoad: false, - errorMessage: errorMessage + errorMessage: errorMessage, }); } - } + }; render() { const { queries } = this.props; @@ -171,7 +172,7 @@ export class DataPanel extends Component { <> {this.renderLoadingStates()} - {({error, errorInfo}) => { + {({ error, errorInfo }) => { if (errorInfo) { this.onError(error.message || DEFAULT_PLUGIN_ERROR); return null; @@ -200,15 +201,11 @@ export class DataPanel extends Component { ); } else if (loading === LoadingState.Error) { return ( - - - + +
+ + +
); } diff --git a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx b/public/app/features/dashboard/dashgrid/DataSourceOption.tsx index 0adfc4abe16..9a3ce527510 100644 --- a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx +++ b/public/app/features/dashboard/dashgrid/DataSourceOption.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface Props { label: string; diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx index b7da81a23f8..dbea7ed59bc 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx @@ -2,9 +2,8 @@ import React, { PureComponent } from 'react'; // Components -import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; +import { CustomScrollbar, PanelOptionsGroup } from '@grafana/ui'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { PanelOptionSection } from './PanelOptionSection'; interface Props { children: JSX.Element; @@ -52,7 +51,7 @@ export class EditorTabBody extends PureComponent { onToggleToolBarView = (item: EditorToolbarView) => { this.setState({ openView: item, - isOpen: !this.state.isOpen, + isOpen: this.state.openView !== item || !this.state.isOpen, }); }; @@ -97,9 +96,9 @@ export class EditorTabBody extends PureComponent { renderOpenView(view: EditorToolbarView) { return ( - + {view.render()} - + ); } diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 2b91e19d83c..a09ff66f114 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -15,7 +15,8 @@ import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types/plugins'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; interface PanelEditorProps { panel: PanelModel; @@ -138,7 +139,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) { return (
onClick(tab)}> - + diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index 331e469a60d..6b6f81fc579 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -1,10 +1,10 @@ import React, { Component } from 'react'; +import Remarkable from 'remarkable'; +import { Tooltip } from '@grafana/ui'; import { PanelModel } from 'app/features/dashboard/panel_model'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; import templateSrv from 'app/features/templating/template_srv'; import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv'; -import Remarkable from 'remarkable'; enum InfoModes { Error = 'Error', @@ -78,12 +78,14 @@ export class PanelHeaderCorner extends Component { {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? ( - - +
+ + +
) : null} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 77ab64b1dba..47c4f358136 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -1,15 +1,15 @@ // Libraries -import React, { PureComponent, SFC } from 'react'; +import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components import 'app/features/panel/metrics_tab'; -import { EditorTabBody, EditorToolbarView} from './EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab'; -import { PanelOptionSection } from './PanelOptionSection'; +import { PanelOptionsGroup } from '@grafana/ui'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -36,12 +36,6 @@ interface State { isAddingMixed: boolean; } -interface LoadingPlaceholderProps { - text: string; -} - -const LoadingPlaceholder: SFC = ({ text }) =>

{text}

; - export class QueriesTab extends PureComponent { element: HTMLElement; component: AngularComponent; @@ -134,7 +128,7 @@ export class QueriesTab extends PureComponent { renderQueryInspector = () => { const { panel } = this.props; - return ; + return ; }; renderHelp = () => { @@ -222,7 +216,7 @@ export class QueriesTab extends PureComponent { return ( <> - +
(this.element = element)} /> @@ -245,10 +239,10 @@ export class QueriesTab extends PureComponent {
- - + + - + ); diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/dashgrid/QueryInspector.tsx index 090bc220bc0..8e490f6b622 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/dashgrid/QueryInspector.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import appEvents from 'app/core/app_events'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; +import { LoadingPlaceholder } from '@grafana/ui'; interface DsQuery { isLoading: boolean; @@ -10,7 +11,6 @@ interface DsQuery { interface Props { panel: any; - LoadingPlaceholder: any; } interface State { @@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent { render() { const { response, isLoading } = this.state.dsQuery; - const { LoadingPlaceholder } = this.props; const { isMocking } = this.state; const openNodes = this.getNrOfOpenNodes(); diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx index bc7102f35dd..ad569a9ff90 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx @@ -9,7 +9,6 @@ import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { PanelOptionSection } from './PanelOptionSection'; // Types import { PanelModel } from '../panel_model'; @@ -62,13 +61,13 @@ export class VisualizationTab extends PureComponent { } return ( - + <> {PanelOptions ? ( ) : (

Visualization has no options

)} -
+ ); } @@ -112,9 +111,9 @@ export class VisualizationTab extends PureComponent { for (let i = 0; i < panelCtrl.editorTabs.length; i++) { template += ` -
` + - (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + - `
+
` + + (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + + `
diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2d5a70b47dd..2fec8e379dd 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -77,7 +77,7 @@ export class PanelModel { repeatPanelId?: number; repeatDirection?: string; repeatedByRow?: boolean; - minSpan?: number; + maxPerRow?: number; collapsed?: boolean; panels?: any; soloMode?: boolean; diff --git a/public/app/features/dashboard/permissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx index c07bef42930..96d0e23adcd 100644 --- a/public/app/features/dashboard/permissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/permissions/DashboardPermissions.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; @@ -70,8 +70,10 @@ export class DashboardPermissions extends PureComponent {

Permissions

- - + +
+ +