mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into tooling/storybook-poc
This commit is contained in:
commit
ad9c96abb5
@ -127,7 +127,7 @@ jobs:
|
|||||||
|
|
||||||
build-all:
|
build-all:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.1
|
- image: grafana/build-container:1.2.2
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -200,51 +200,51 @@ jobs:
|
|||||||
- dist/grafana*
|
- dist/grafana*
|
||||||
|
|
||||||
grafana-docker-master:
|
grafana-docker-master:
|
||||||
docker:
|
machine:
|
||||||
- image: docker:stable-git
|
image: circleci/classic:201808-01
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker info
|
- 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: 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: 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"
|
- run: cd packaging/docker && ./build-enterprise.sh "master"
|
||||||
|
|
||||||
|
|
||||||
grafana-docker-pr:
|
grafana-docker-pr:
|
||||||
docker:
|
machine:
|
||||||
- image: docker:stable-git
|
image: circleci/classic:201808-01
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker info
|
- 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}"
|
- run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
|
||||||
|
|
||||||
grafana-docker-release:
|
grafana-docker-release:
|
||||||
docker:
|
machine:
|
||||||
- image: docker:stable-git
|
image: circleci/classic:201808-01
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker info
|
- 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 "${CIRCLE_TAG}"
|
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
|
||||||
- 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: 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}"
|
- run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
|
||||||
|
|
||||||
build-enterprise:
|
build-enterprise:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.1
|
- image: grafana/build-container:1.2.2
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -276,7 +276,7 @@ jobs:
|
|||||||
|
|
||||||
build-all-enterprise:
|
build-all-enterprise:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.1
|
- image: grafana/build-container:1.2.2
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -323,7 +323,7 @@ jobs:
|
|||||||
|
|
||||||
deploy-enterprise-master:
|
deploy-enterprise-master:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.1.0
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@ -346,7 +346,7 @@ jobs:
|
|||||||
|
|
||||||
deploy-enterprise-release:
|
deploy-enterprise-release:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.1.0
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@ -365,10 +365,20 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Deploy to Grafana.com
|
name: Deploy to Grafana.com
|
||||||
command: './scripts/build/publish.sh --enterprise'
|
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:
|
deploy-master:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.1.0
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@ -398,8 +408,9 @@ jobs:
|
|||||||
|
|
||||||
deploy-release:
|
deploy-release:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.1.0
|
||||||
steps:
|
steps:
|
||||||
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- run:
|
- run:
|
||||||
@ -417,6 +428,15 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Deploy to Grafana.com
|
name: Deploy to Grafana.com
|
||||||
command: './scripts/build/publish.sh'
|
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:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
@ -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)
|
* **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)
|
* **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)
|
* **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
|
### Bug fixes
|
||||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
||||||
|
@ -133,6 +133,8 @@ If you have any idea for an improvement or found a bug, do not hesitate to open
|
|||||||
And if you have time clone this repo and submit a pull request and help me make Grafana
|
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
|
## Plugin development
|
||||||
|
|
||||||
Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to
|
Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to
|
||||||
|
2
build.go
2
build.go
@ -164,6 +164,8 @@ func makeLatestDistCopies() {
|
|||||||
"_amd64.deb": "dist/grafana_latest_amd64.deb",
|
"_amd64.deb": "dist/grafana_latest_amd64.deb",
|
||||||
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
|
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
|
||||||
".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
|
".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 {
|
for _, file := range files {
|
||||||
|
@ -69,6 +69,7 @@ reporting-disabled = false
|
|||||||
|
|
||||||
unix-socket-enabled = false # enable http service over unix domain socket
|
unix-socket-enabled = false # enable http service over unix domain socket
|
||||||
# bind-socket = "/var/run/influxdb.sock"
|
# bind-socket = "/var/run/influxdb.sock"
|
||||||
|
flux-enabled = true
|
||||||
|
|
||||||
[subscriber]
|
[subscriber]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
|||||||
"list": []
|
"list": []
|
||||||
},
|
},
|
||||||
"refresh": "5s",
|
"refresh": "5s",
|
||||||
"schemaVersion": 16,
|
"schemaVersion": 17,
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"links": []
|
"links": []
|
||||||
}
|
}
|
||||||
|
@ -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
|
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
|
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.
|
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.
|
You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
"@types/jquery": "^1.10.35",
|
"@types/jquery": "^1.10.35",
|
||||||
"@types/node": "^8.0.31",
|
"@types/node": "^8.0.31",
|
||||||
"@types/react": "^16.7.6",
|
"@types/react": "^16.7.6",
|
||||||
"@types/react-custom-scrollbars": "^4.0.5",
|
|
||||||
"@types/react-dom": "^16.0.9",
|
"@types/react-dom": "^16.0.9",
|
||||||
"@types/react-select": "^2.0.4",
|
"@types/react-select": "^2.0.4",
|
||||||
"angular-mocks": "1.6.6",
|
"angular-mocks": "1.6.6",
|
||||||
@ -65,6 +64,7 @@
|
|||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"jest": "^23.6.0",
|
"jest": "^23.6.0",
|
||||||
|
"jest-date-mock": "^1.0.6",
|
||||||
"lint-staged": "^6.0.0",
|
"lint-staged": "^6.0.0",
|
||||||
"load-grunt-tasks": "3.5.2",
|
"load-grunt-tasks": "3.5.2",
|
||||||
"mini-css-extract-plugin": "^0.4.0",
|
"mini-css-extract-plugin": "^0.4.0",
|
||||||
@ -72,8 +72,8 @@
|
|||||||
"ng-annotate-loader": "^0.6.1",
|
"ng-annotate-loader": "^0.6.1",
|
||||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||||
"ngtemplate-loader": "^2.0.1",
|
"ngtemplate-loader": "^2.0.1",
|
||||||
"npm": "^5.4.2",
|
|
||||||
"node-sass": "^4.11.0",
|
"node-sass": "^4.11.0",
|
||||||
|
"npm": "^5.4.2",
|
||||||
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
||||||
"phantomjs-prebuilt": "^2.1.15",
|
"phantomjs-prebuilt": "^2.1.15",
|
||||||
"postcss-browser-reporter": "^0.5.0",
|
"postcss-browser-reporter": "^0.5.0",
|
||||||
@ -167,7 +167,6 @@
|
|||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"rc-cascader": "^0.14.0",
|
"rc-cascader": "^0.14.0",
|
||||||
"react": "^16.6.3",
|
"react": "^16.6.3",
|
||||||
"react-custom-scrollbars": "^4.2.1",
|
|
||||||
"react-dom": "^16.6.3",
|
"react-dom": "^16.6.3",
|
||||||
"react-grid-layout": "0.16.6",
|
"react-grid-layout": "0.16.6",
|
||||||
"react-highlight-words": "0.11.0",
|
"react-highlight-words": "0.11.0",
|
||||||
|
@ -17,11 +17,15 @@
|
|||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"react": "^16.6.3",
|
"react": "^16.6.3",
|
||||||
|
"react-custom-scrollbars": "^4.2.1",
|
||||||
"react-dom": "^16.6.3",
|
"react-dom": "^16.6.3",
|
||||||
"react-highlight-words": "0.11.0",
|
"react-highlight-words": "0.11.0",
|
||||||
"react-popper": "^1.3.0",
|
"react-popper": "^1.3.0",
|
||||||
"react-transition-group": "^2.2.1",
|
"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": {
|
"devDependencies": {
|
||||||
"@storybook/addon-info": "^4.1.4",
|
"@storybook/addon-info": "^4.1.4",
|
||||||
@ -33,8 +37,14 @@
|
|||||||
"@types/node": "^10.12.18",
|
"@types/node": "^10.12.18",
|
||||||
"@types/react": "^16.7.6",
|
"@types/react": "^16.7.6",
|
||||||
"@types/storybook__react": "^4.0.0",
|
"@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",
|
"awesome-typescript-loader": "^5.2.1",
|
||||||
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
||||||
|
"react-test-renderer": "^16.7.0",
|
||||||
"typescript": "^3.2.2"
|
"typescript": "^3.2.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { ColorPalette } from '../components/colorpicker/ColorPalette';
|
import { ColorPalette } from './ColorPalette';
|
||||||
|
|
||||||
describe('CollorPalette', () => {
|
describe('CollorPalette', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { sortedColors } from 'app/core/utils/colors';
|
import { sortedColors } from '../../utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
color: string;
|
color: string;
|
||||||
@ -9,13 +9,13 @@ export interface Props {
|
|||||||
export class ColorPalette extends React.Component<Props, any> {
|
export class ColorPalette extends React.Component<Props, any> {
|
||||||
paletteColors: string[];
|
paletteColors: string[];
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.paletteColors = sortedColors;
|
this.paletteColors = sortedColors;
|
||||||
this.onColorSelect = this.onColorSelect.bind(this);
|
this.onColorSelect = this.onColorSelect.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorSelect(color) {
|
onColorSelect(color: string) {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.onColorSelect(color);
|
this.props.onColorSelect(color);
|
||||||
};
|
};
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
color: string;
|
color: string;
|
||||||
@ -10,7 +9,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ColorPicker extends React.Component<Props, any> {
|
export class ColorPicker extends React.Component<Props, any> {
|
||||||
pickerElem: HTMLElement;
|
pickerElem: HTMLElement | null;
|
||||||
colorPickerDrop: any;
|
colorPickerDrop: any;
|
||||||
|
|
||||||
openColorPicker = () => {
|
openColorPicker = () => {
|
||||||
@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
ReactDOM.render(dropContent, dropContentElem);
|
ReactDOM.render(dropContent, dropContentElem);
|
||||||
|
|
||||||
const drop = new Drop({
|
const drop = new Drop({
|
||||||
target: this.pickerElem,
|
target: this.pickerElem as Element,
|
||||||
content: dropContentElem,
|
content: dropContentElem,
|
||||||
position: 'top center',
|
position: 'top center',
|
||||||
classes: 'drop-popover',
|
classes: 'drop-popover',
|
||||||
@ -28,6 +27,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
hoverCloseDelay: 200,
|
hoverCloseDelay: 200,
|
||||||
tetherOptions: {
|
tetherOptions: {
|
||||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||||
|
attachment: 'bottom center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
onColorSelect = color => {
|
onColorSelect = (color: string) => {
|
||||||
this.props.onChange(color);
|
this.props.onChange(color);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,8 +59,3 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react2AngularDirective('colorPicker', ColorPicker, [
|
|
||||||
'color',
|
|
||||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
|
||||||
]);
|
|
@ -14,7 +14,7 @@ export interface Props {
|
|||||||
export class ColorPickerPopover extends React.Component<Props, any> {
|
export class ColorPickerPopover extends React.Component<Props, any> {
|
||||||
pickerNavElem: any;
|
pickerNavElem: any;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
tab: 'palette',
|
tab: 'palette',
|
||||||
@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPickerNavElem(elem) {
|
setPickerNavElem(elem: any) {
|
||||||
this.pickerNavElem = $(elem);
|
this.pickerNavElem = $(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
setColor(color) {
|
setColor(color: string) {
|
||||||
const newColor = tinycolor(color);
|
const newColor = tinycolor(color);
|
||||||
if (newColor.isValid()) {
|
if (newColor.isValid()) {
|
||||||
this.setState({
|
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
|
||||||
color: newColor.toString(),
|
|
||||||
colorString: newColor.toString(),
|
|
||||||
});
|
|
||||||
this.props.onColorSelect(color);
|
this.props.onColorSelect(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleColorSelected(color) {
|
sampleColorSelected(color: string) {
|
||||||
this.setColor(color);
|
this.setColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
spectrumColorSelected(color) {
|
spectrumColorSelected(color: any) {
|
||||||
const rgbColor = color.toRgbString();
|
const rgbColor = color.toRgbString();
|
||||||
this.setColor(rgbColor);
|
this.setColor(rgbColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorStringChange(e) {
|
onColorStringChange(e: any) {
|
||||||
const colorString = e.target.value;
|
const colorString = e.target.value;
|
||||||
this.setState({
|
this.setState({ colorString: colorString });
|
||||||
colorString: colorString,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newColor = tinycolor(colorString);
|
const newColor = tinycolor(colorString);
|
||||||
if (newColor.isValid()) {
|
if (newColor.isValid()) {
|
||||||
// Update only color state
|
// Update only color state
|
||||||
const newColorString = newColor.toString();
|
const newColorString = newColor.toString();
|
||||||
this.setState({
|
this.setState({ color: newColorString });
|
||||||
color: newColorString,
|
|
||||||
});
|
|
||||||
this.props.onColorSelect(newColorString);
|
this.props.onColorSelect(newColorString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorStringBlur(e) {
|
onColorStringBlur(e: any) {
|
||||||
const colorString = e.target.value;
|
const colorString = e.target.value;
|
||||||
this.setColor(colorString);
|
this.setColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.pickerNavElem.find('li:first').addClass('active');
|
this.pickerNavElem.find('li:first').addClass('active');
|
||||||
this.pickerNavElem.on('show', e => {
|
this.pickerNavElem.on('show', (e: any) => {
|
||||||
// use href attr (#name => name)
|
// use href attr (#name => name)
|
||||||
const tab = e.target.hash.slice(1);
|
const tab = e.target.hash.slice(1);
|
||||||
this.setState({
|
this.setState({ tab: tab });
|
||||||
tab: tab,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
|||||||
onToggleAxis: () => {},
|
onToggleAxis: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: SeriesColorPickerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
|||||||
remove: true,
|
remove: true,
|
||||||
tetherOptions: {
|
tetherOptions: {
|
||||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||||
|
attachment: 'bottom center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
|
||||||
|
|
||||||
export interface SeriesColorPickerPopoverProps {
|
export interface SeriesColorPickerPopoverProps {
|
||||||
color: string;
|
color: string;
|
||||||
@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPic
|
|||||||
|
|
||||||
interface AxisSelectorProps {
|
interface AxisSelectorProps {
|
||||||
yaxis: number;
|
yaxis: number;
|
||||||
onToggleAxis: () => void;
|
onToggleAxis?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AxisSelectorState {
|
interface AxisSelectorState {
|
||||||
@ -30,7 +29,7 @@ interface AxisSelectorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
||||||
constructor(props) {
|
constructor(props: AxisSelectorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
yaxis: this.props.yaxis,
|
yaxis: this.props.yaxis,
|
||||||
@ -42,8 +41,11 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
|||||||
this.setState({
|
this.setState({
|
||||||
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.props.onToggleAxis) {
|
||||||
this.props.onToggleAxis();
|
this.props.onToggleAxis();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
||||||
@ -62,9 +64,3 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
|
|
||||||
'series',
|
|
||||||
'onColorChange',
|
|
||||||
'onToggleAxis',
|
|
||||||
]);
|
|
@ -13,17 +13,17 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
|||||||
elem: any;
|
elem: any;
|
||||||
isMoving: boolean;
|
isMoving: boolean;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||||
this.setComponentElem = this.setComponentElem.bind(this);
|
this.setComponentElem = this.setComponentElem.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setComponentElem(elem) {
|
setComponentElem(elem: any) {
|
||||||
this.elem = $(elem);
|
this.elem = $(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpectrumMove(color) {
|
onSpectrumMove(color: any) {
|
||||||
this.isMoving = true;
|
this.isMoving = true;
|
||||||
this.props.onColorSelect(color);
|
this.props.onColorSelect(color);
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
|||||||
this.elem.spectrum('set', this.props.color);
|
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
|
// 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
|
// 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
|
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
@ -12,7 +12,7 @@ interface Props {
|
|||||||
/**
|
/**
|
||||||
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
||||||
*/
|
*/
|
||||||
class CustomScrollbar extends PureComponent<Props> {
|
export class CustomScrollbar extends PureComponent<Props> {
|
||||||
static defaultProps: Partial<Props> = {
|
static defaultProps: Partial<Props> = {
|
||||||
customClassName: 'custom-scrollbars',
|
customClassName: 'custom-scrollbars',
|
||||||
autoHide: true,
|
autoHide: true,
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
interface LoadingPlaceholderProps {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
|
||||||
|
<div className="gf-form-group">
|
||||||
|
{text} <i className="fa fa-spinner fa-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,15 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cols?: number;
|
||||||
|
children: JSX.Element[] | JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelOptionsGrid: SFC<Props> = ({ children }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel-options-grid">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -7,11 +7,11 @@ interface Props {
|
|||||||
children: JSX.Element | JSX.Element[];
|
children: JSX.Element | JSX.Element[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelOptionSection: SFC<Props> = props => {
|
export const PanelOptionsGroup: SFC<Props> = props => {
|
||||||
return (
|
return (
|
||||||
<div className="panel-option-section">
|
<div className="panel-options-group">
|
||||||
{props.title && (
|
{props.title && (
|
||||||
<div className="panel-option-section__header">
|
<div className="panel-options-group__header">
|
||||||
{props.title}
|
{props.title}
|
||||||
{props.onClose && (
|
{props.onClose && (
|
||||||
<button className="btn btn-link" onClick={props.onClose}>
|
<button className="btn btn-link" onClick={props.onClose}>
|
||||||
@ -20,7 +20,7 @@ export const PanelOptionSection: SFC<Props> = props => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="panel-option-section__body">{props.children}</div>
|
<div className="panel-options-group__body">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -6,11 +6,11 @@ interface Props {
|
|||||||
root?: HTMLElement;
|
root?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BodyPortal extends PureComponent<Props> {
|
export class Portal extends PureComponent<Props> {
|
||||||
node: HTMLElement = document.createElement('div');
|
node: HTMLElement = document.createElement('div');
|
||||||
portalRoot: HTMLElement;
|
portalRoot: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
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 { components } from '@torkelo/react-select';
|
||||||
|
|
||||||
export const IndicatorsContainer = props => {
|
export const IndicatorsContainer = (props: any) => {
|
||||||
const isOpen = props.selectProps.menuIsOpen;
|
const isOpen = props.selectProps.menuIsOpen;
|
||||||
return (
|
return (
|
||||||
<components.IndicatorsContainer {...props}>
|
<components.IndicatorsContainer {...props}>
|
@ -1,5 +1,9 @@
|
|||||||
import React from 'react';
|
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 { components } from '@torkelo/react-select';
|
||||||
|
// @ts-ignore
|
||||||
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
@ -1,17 +1,22 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { PureComponent } from 'react';
|
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';
|
import { default as ReactSelect } from '@torkelo/react-select';
|
||||||
|
// @ts-ignore
|
||||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||||
|
// @ts-ignore
|
||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Option, SingleValue } from './PickerOption';
|
import { SelectOption, SingleValue } from './SelectOption';
|
||||||
import OptionGroup from './OptionGroup';
|
import SelectOptionGroup from './SelectOptionGroup';
|
||||||
import IndicatorsContainer from './IndicatorsContainer';
|
import IndicatorsContainer from './IndicatorsContainer';
|
||||||
import NoOptionsMessage from './NoOptionsMessage';
|
import NoOptionsMessage from './NoOptionsMessage';
|
||||||
import ResetStyles from './ResetStyles';
|
import resetSelectStyles from './resetSelectStyles';
|
||||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
import { CustomScrollbar } from '@grafana/ui';
|
||||||
|
|
||||||
export interface SelectOptionItem {
|
export interface SelectOptionItem {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -53,7 +58,7 @@ interface AsyncProps {
|
|||||||
loadingMessage?: () => string;
|
loadingMessage?: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuList = props => {
|
export const MenuList = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<components.MenuList {...props}>
|
<components.MenuList {...props}>
|
||||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
||||||
@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={selectClassNames}
|
className={selectClassNames}
|
||||||
components={{
|
components={{
|
||||||
Option,
|
Option: SelectOption,
|
||||||
SingleValue,
|
SingleValue,
|
||||||
IndicatorsContainer,
|
IndicatorsContainer,
|
||||||
MenuList,
|
MenuList,
|
||||||
Group: OptionGroup,
|
Group: SelectOptionGroup,
|
||||||
}}
|
}}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={placeholder || 'Choose'}
|
placeholder={placeholder || 'Choose'}
|
||||||
styles={ResetStyles}
|
styles={resetSelectStyles()}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
defaultOptions={defaultOptions}
|
defaultOptions={defaultOptions}
|
||||||
placeholder={placeholder || 'Choose'}
|
placeholder={placeholder || 'Choose'}
|
||||||
styles={ResetStyles}
|
styles={resetSelectStyles()}
|
||||||
loadingMessage={loadingMessage}
|
loadingMessage={loadingMessage}
|
||||||
noOptionsMessage={noOptionsMessage}
|
noOptionsMessage={noOptionsMessage}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
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<any> = {
|
||||||
cx: jest.fn(),
|
cx: jest.fn(),
|
||||||
clearValue: jest.fn(),
|
clearValue: jest.fn(),
|
||||||
onSelect: jest.fn(),
|
|
||||||
getStyles: jest.fn(),
|
getStyles: jest.fn(),
|
||||||
getValue: jest.fn(),
|
getValue: jest.fn(),
|
||||||
hasValue: true,
|
hasValue: true,
|
||||||
@ -18,21 +19,31 @@ const model = {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
innerRef: null,
|
innerRef: null,
|
||||||
innerProps: null,
|
innerProps: {
|
||||||
label: 'Option label',
|
id: '',
|
||||||
type: null,
|
key: '',
|
||||||
children: 'Model title',
|
onClick: jest.fn(),
|
||||||
data: {
|
onMouseOver: jest.fn(),
|
||||||
title: 'Model title',
|
tabIndex: 1,
|
||||||
imgUrl: 'url/to/avatar',
|
|
||||||
label: 'User picker label',
|
|
||||||
},
|
},
|
||||||
|
label: 'Option label',
|
||||||
|
type: 'option',
|
||||||
|
children: 'Model title',
|
||||||
className: 'class-for-user-picker',
|
className: 'class-for-user-picker',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('PickerOption', () => {
|
describe('SelectOption', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer.create(<PickerOption {...model} />).toJSON();
|
const tree = renderer
|
||||||
|
.create(
|
||||||
|
<SelectOption
|
||||||
|
{...model}
|
||||||
|
data={{
|
||||||
|
imgUrl: 'url/to/avatar',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
.toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,4 +1,7 @@
|
|||||||
import React from 'react';
|
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 { components } from '@torkelo/react-select';
|
||||||
import { OptionProps } from 'react-select/lib/components/Option';
|
import { OptionProps } from 'react-select/lib/components/Option';
|
||||||
|
|
||||||
@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Option = (props: ExtendedOptionProps) => {
|
export const SelectOption = (props: ExtendedOptionProps) => {
|
||||||
const { children, isSelected, data } = props;
|
const { children, isSelected, data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// was not able to type this without typescript error
|
// was not able to type this without typescript error
|
||||||
export const SingleValue = props => {
|
export const SingleValue = (props: any) => {
|
||||||
const { children, data } = props;
|
const { children, data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -41,4 +44,4 @@ export const SingleValue = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Option;
|
export default SelectOption;
|
@ -9,7 +9,7 @@ interface State {
|
|||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||||
state = {
|
state = {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
};
|
};
|
||||||
@ -24,7 +24,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(nextProps) {
|
componentDidUpdate(nextProps: ExtendedGroupProps) {
|
||||||
if (nextProps.selectProps.inputValue !== '') {
|
if (nextProps.selectProps.inputValue !== '') {
|
||||||
this.setState({ expanded: true });
|
this.setState({ expanded: true });
|
||||||
}
|
}
|
@ -1,7 +1,12 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`PickerOption renders correctly 1`] = `
|
exports[`SelectOption renders correctly 1`] = `
|
||||||
<div>
|
<div
|
||||||
|
id=""
|
||||||
|
onClick={[MockFunction]}
|
||||||
|
onMouseOver={[MockFunction]}
|
||||||
|
tabIndex={1}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__desc-option"
|
className="gf-form-select-box__desc-option"
|
||||||
>
|
>
|
@ -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: () => ({}),
|
||||||
|
};
|
||||||
|
}
|
@ -1,22 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import Thresholds from './Thresholds';
|
|
||||||
import { defaultProps, OptionsProps } from './module';
|
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||||
import { BasicGaugeColor } from 'app/types';
|
import { BasicGaugeColor } from '../../types';
|
||||||
import { PanelOptionsProps } from '@grafana/ui';
|
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: PanelOptionsProps<OptionsProps> = {
|
const props: Props = {
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
options: {
|
|
||||||
...defaultProps.options,
|
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
return shallow(<Thresholds {...props} />).instance() as Thresholds;
|
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Add threshold', () => {
|
describe('Add threshold', () => {
|
||||||
@ -30,10 +26,7 @@ describe('Add threshold', () => {
|
|||||||
|
|
||||||
it('should add another threshold above a first', () => {
|
it('should add another threshold above a first', () => {
|
||||||
const instance = setup({
|
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);
|
instance.onAddThreshold(1);
|
@ -1,31 +1,38 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor, { ColorInput } from 'tinycolor2';
|
||||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
|
||||||
import { OptionModuleProps } from './module';
|
import { Threshold, BasicGaugeColor } from '../../types';
|
||||||
import { BasicGaugeColor, Threshold } from 'app/types';
|
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||||
|
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
thresholds: Threshold[];
|
||||||
|
onChange: (thresholds: Threshold[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
baseColor: string;
|
baseColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
|
||||||
thresholds: props.options.thresholds,
|
|
||||||
baseColor: props.options.baseColor,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddThreshold = index => {
|
onAddThreshold = (index: number) => {
|
||||||
const { maxValue, minValue } = this.props.options;
|
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 { thresholds } = this.state;
|
||||||
|
|
||||||
const newThresholds = thresholds.map(threshold => {
|
const newThresholds = thresholds.map(threshold => {
|
||||||
if (threshold.index >= index) {
|
if (threshold.index >= index) {
|
||||||
threshold = { ...threshold, index: threshold.index + 1 };
|
threshold = {
|
||||||
|
...threshold,
|
||||||
|
index: threshold.index + 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return threshold;
|
return threshold;
|
||||||
@ -47,27 +54,32 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
if (index === 0 && thresholds.length === 0) {
|
if (index === 0 && thresholds.length === 0) {
|
||||||
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
|
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
|
||||||
} else {
|
} 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(
|
this.setState(
|
||||||
{
|
{
|
||||||
thresholds: this.sortThresholds([...newThresholds, { index: index, value: value, color: color }]),
|
thresholds: this.sortThresholds([
|
||||||
|
...newThresholds,
|
||||||
|
{
|
||||||
|
index,
|
||||||
|
value: value as number,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
() => this.updateGauge()
|
() => this.updateGauge()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemoveThreshold = threshold => {
|
onRemoveThreshold = (threshold: Threshold) => {
|
||||||
this.setState(
|
this.setState(
|
||||||
prevState => ({
|
prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
|
||||||
thresholds: prevState.thresholds.filter(t => t !== threshold),
|
|
||||||
}),
|
|
||||||
() => this.updateGauge()
|
() => this.updateGauge()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeThresholdValue = (event, threshold) => {
|
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
||||||
const { thresholds } = this.state;
|
const { thresholds } = this.state;
|
||||||
|
|
||||||
const newThresholds = thresholds.map(t => {
|
const newThresholds = thresholds.map(t => {
|
||||||
@ -78,12 +90,10 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({ thresholds: newThresholds });
|
||||||
thresholds: newThresholds,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeThresholdColor = (threshold, color) => {
|
onChangeThresholdColor = (threshold: Threshold, color: string) => {
|
||||||
const { thresholds } = this.state;
|
const { thresholds } = this.state;
|
||||||
|
|
||||||
const newThresholds = thresholds.map(t => {
|
const newThresholds = thresholds.map(t => {
|
||||||
@ -102,20 +112,18 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeBaseColor = color => this.props.onChange({ ...this.props.options, baseColor: color });
|
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
|
||||||
thresholds: this.sortThresholds(prevState.thresholds),
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.updateGauge();
|
this.updateGauge();
|
||||||
};
|
};
|
||||||
|
|
||||||
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 thresholds.sort((t1, t2) => {
|
||||||
return t2.value - t1.value;
|
return t2.value - t1.value;
|
||||||
});
|
});
|
||||||
@ -160,20 +168,8 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
return thresholds.map((t, i) => {
|
return thresholds.map((t, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={`${t.value}-${i}`} className="indicator-section">
|
<div key={`${t.value}-${i}`} className="indicator-section">
|
||||||
<div
|
<div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||||
onClick={() => this.onAddThreshold(t.index + 1)}
|
<div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||||
style={{
|
|
||||||
height: '50%',
|
|
||||||
backgroundColor: t.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
onClick={() => this.onAddThreshold(t.index)}
|
|
||||||
style={{
|
|
||||||
height: '50%',
|
|
||||||
backgroundColor: t.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -184,14 +180,14 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
<div className="indicator-section" style={{ height: '100%' }}>
|
<div className="indicator-section" style={{ height: '100%' }}>
|
||||||
<div
|
<div
|
||||||
onClick={() => this.onAddThreshold(0)}
|
onClick={() => this.onAddThreshold(0)}
|
||||||
style={{ height: '100%', backgroundColor: this.props.options.baseColor }}
|
style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBase() {
|
renderBase() {
|
||||||
const { baseColor } = this.props.options;
|
const baseColor = BasicGaugeColor.Green;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="threshold-row threshold-row-base">
|
<div className="threshold-row threshold-row-base">
|
||||||
@ -209,8 +205,7 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="section gf-form-group">
|
<PanelOptionsGroup title="Thresholds">
|
||||||
<h5 className="section-heading">Thresholds</h5>
|
|
||||||
<div className="thresholds">
|
<div className="thresholds">
|
||||||
<div className="color-indicators">
|
<div className="color-indicators">
|
||||||
{this.renderIndicator()}
|
{this.renderIndicator()}
|
||||||
@ -221,7 +216,7 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
|||||||
{this.renderBase()}
|
{this.renderBase()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PanelOptionsGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,13 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import Portal from 'app/core/components/Portal/Portal';
|
import * as PopperJS from 'popper.js';
|
||||||
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
|
import { Manager, Popper as ReactPopper } from 'react-popper';
|
||||||
|
import { Portal } from '@grafana/ui';
|
||||||
import Transition from 'react-transition-group/Transition';
|
import Transition from 'react-transition-group/Transition';
|
||||||
|
|
||||||
export enum Themes {
|
export enum Themes {
|
||||||
Default = 'popper__background--default',
|
Default = 'popper__background--default',
|
||||||
Error = 'popper__background--error',
|
Error = 'popper__background--error',
|
||||||
|
Brand = 'popper__background--brand',
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTransitionStyles = {
|
const defaultTransitionStyles = {
|
||||||
@ -13,45 +15,40 @@ const defaultTransitionStyles = {
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const transitionStyles = {
|
const transitionStyles: {[key: string]: object} = {
|
||||||
exited: { opacity: 0 },
|
exited: { opacity: 0 },
|
||||||
entering: { opacity: 0 },
|
entering: { opacity: 0 },
|
||||||
entered: { opacity: 1 },
|
entered: { opacity: 1 },
|
||||||
exiting: { opacity: 0 },
|
exiting: { opacity: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props extends React.DOMAttributes<HTMLDivElement> {
|
||||||
renderContent: (content: any) => any;
|
renderContent: (content: any) => any;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
placement?: any;
|
placement?: PopperJS.Placement;
|
||||||
content: string | ((props: any) => JSX.Element);
|
content: string | ((props: any) => JSX.Element);
|
||||||
refClassName?: string;
|
referenceElement: PopperJS.ReferenceObject;
|
||||||
theme?: Themes;
|
theme?: Themes;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Popper extends PureComponent<Props> {
|
class Popper extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { children, renderContent, show, placement, refClassName, theme } = this.props;
|
const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
|
||||||
const { content } = this.props;
|
const { content } = this.props;
|
||||||
|
|
||||||
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
|
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<Manager>
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||||
{transitionState => (
|
{transitionState => (
|
||||||
<Portal>
|
<Portal>
|
||||||
<ReactPopper placement={placement}>
|
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
|
||||||
{({ ref, style, placement, arrowProps }) => {
|
{({ ref, style, placement, arrowProps }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as PopperJS from 'popper.js';
|
||||||
|
import { Themes } from './Popper';
|
||||||
|
|
||||||
|
type PopperContent = string | (() => 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<Props, State> {
|
||||||
|
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 <ReactComponent />;
|
||||||
|
}
|
||||||
|
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;
|
@ -1,13 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import Tooltip from './Tooltip';
|
import { Tooltip } from './Tooltip';
|
||||||
|
|
||||||
describe('Tooltip', () => {
|
describe('Tooltip', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create(
|
.create(
|
||||||
<Tooltip className="test-class" placement="auto" content="Tooltip text">
|
<Tooltip placement="auto" content="Tooltip text">
|
||||||
<a href="http://www.grafana.com">Link with tooltip</a>
|
<a className="test-class" href="http://www.grafana.com">
|
||||||
|
Link with tooltip
|
||||||
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
.toJSON();
|
.toJSON();
|
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
@ -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<PopperJS.ReferenceObject>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopperController {...controllerProps}>
|
||||||
|
{(showPopper, hidePopper, popperProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tooltipTriggerRef.current && (
|
||||||
|
<Popper
|
||||||
|
{...popperProps}
|
||||||
|
onMouseEnter={showPopper}
|
||||||
|
onMouseLeave={hidePopper}
|
||||||
|
referenceElement={tooltipTriggerRef.current}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
ref: tooltipTriggerRef,
|
||||||
|
onMouseEnter: showPopper,
|
||||||
|
onMouseLeave: hidePopper,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</PopperController>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,13 @@
|
|||||||
$popper-margin-from-ref: 5px;
|
$popper-margin-from-ref: 5px;
|
||||||
|
|
||||||
|
|
||||||
|
@mixin popper-theme($backgroundColor, $arrowColor) {
|
||||||
|
background: $backgroundColor;
|
||||||
|
.popper__arrow {
|
||||||
|
border-color: $arrowColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.popper {
|
.popper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: $zindex-tooltip;
|
z-index: $zindex-tooltip;
|
||||||
@ -16,10 +24,12 @@ $popper-margin-from-ref: 5px;
|
|||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
&.popper__background--error {
|
&.popper__background--error {
|
||||||
background: $tooltipBackgroundError;
|
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
|
||||||
.popper__arrow {
|
|
||||||
border-color: $tooltipBackgroundError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.popper__background--brand {
|
||||||
|
@include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand);
|
||||||
|
@include gradient-vertical($red, $orange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Tooltip renders correctly 1`] = `
|
||||||
|
<a
|
||||||
|
className="test-class"
|
||||||
|
href="http://www.grafana.com"
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
Link with tooltip
|
||||||
|
</a>
|
||||||
|
`;
|
@ -1 +1,7 @@
|
|||||||
|
@import 'CustomScrollbar/CustomScrollbar';
|
||||||
@import 'DeleteButton/DeleteButton';
|
@import 'DeleteButton/DeleteButton';
|
||||||
|
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||||
|
@import 'Tooltip/Tooltip';
|
||||||
|
@import 'Select/Select';
|
||||||
|
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||||
|
@ -1 +1,20 @@
|
|||||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
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';
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
|
|
@ -1,5 +1,3 @@
|
|||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './visualizations';
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './forms';
|
|
||||||
|
16
packages/grafana-ui/src/types/gauge.ts
Normal file
16
packages/grafana-ui/src/types/gauge.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { RangeMap, Threshold, ValueMap } from './panel';
|
||||||
|
|
||||||
|
export interface GaugeOptions {
|
||||||
|
baseColor: string;
|
||||||
|
decimals: number;
|
||||||
|
mappings: Array<RangeMap | ValueMap>;
|
||||||
|
maxValue: number;
|
||||||
|
minValue: number;
|
||||||
|
prefix: string;
|
||||||
|
showThresholdLabels: boolean;
|
||||||
|
showThresholdMarkers: boolean;
|
||||||
|
stat: string;
|
||||||
|
suffix: string;
|
||||||
|
thresholds: Threshold[];
|
||||||
|
unit: string;
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export * from './series';
|
export * from './series';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './panel';
|
export * from './panel';
|
||||||
|
export * from './gauge';
|
||||||
|
@ -29,3 +29,35 @@ export interface PanelMenuItem {
|
|||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
subMenu?: PanelMenuItem[];
|
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;
|
||||||
|
}
|
||||||
|
93
packages/grafana-ui/src/utils/colors.ts
Normal file
93
packages/grafana-ui/src/utils/colors.ts
Normal file
@ -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);
|
@ -1 +1,3 @@
|
|||||||
export * from './processTimeSeries';
|
export * from './processTimeSeries';
|
||||||
|
export * from './valueFormats/valueFormats';
|
||||||
|
export * from './colors';
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
322
packages/grafana-ui/src/utils/valueFormats/categories.ts
Normal file
322
packages/grafana-ui/src/utils/valueFormats/categories.ts
Normal file
@ -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') },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
312
packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts
Normal file
312
packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts
Normal file
@ -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();
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { currency } from './symbolFormatters';
|
||||||
|
|
||||||
|
describe('Currency', () => {
|
||||||
|
it('should format as usd', () => {
|
||||||
|
expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K');
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
166
packages/grafana-ui/src/utils/valueFormats/valueFormats.ts
Normal file
166
packages/grafana-ui/src/utils/valueFormats/valueFormats.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
export { Graph } from './Graph/Graph';
|
|
@ -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"
|
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
|
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_UID="472"
|
||||||
ARG GF_GID="472"
|
ARG GF_GID="472"
|
||||||
|
@ -8,6 +8,5 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
|||||||
./push_to_docker_hub.sh "$_grafana_version"
|
./push_to_docker_hub.sh "$_grafana_version"
|
||||||
|
|
||||||
if echo "$_grafana_version" | grep -q "^master-"; then
|
if echo "$_grafana_version" | grep -q "^master-"; then
|
||||||
apk add --no-cache curl
|
|
||||||
./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
|
./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
|
||||||
fi
|
fi
|
||||||
|
@ -1,25 +1,49 @@
|
|||||||
#!/bin/sh
|
#!/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 the tag starts with v, treat this as a official release
|
||||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||||
_docker_repo=${2:-grafana/grafana}
|
|
||||||
else
|
else
|
||||||
_grafana_version=$_grafana_tag
|
_grafana_version=$_grafana_tag
|
||||||
_docker_repo=${2:-grafana/grafana-dev}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Building ${_docker_repo}:${_grafana_version}"
|
echo "Building ${_docker_repo}:${_grafana_version}"
|
||||||
|
|
||||||
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
|
|
||||||
|
# Build grafana image for a specific arch
|
||||||
|
docker_build () {
|
||||||
|
base_image=$1
|
||||||
|
grafana_tgz=$2
|
||||||
|
tag=$3
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
--tag "${_docker_repo}:${_grafana_version}" \
|
--build-arg BASE_IMAGE=${base_image} \
|
||||||
|
--build-arg GRAFANA_TGZ=${grafana_tgz} \
|
||||||
|
--tag "${tag}" \
|
||||||
--no-cache=true .
|
--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
|
# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
|
||||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||||
docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
|
docker_tag_all "${_docker_repo}" "latest"
|
||||||
else
|
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
|
fi
|
||||||
|
@ -1,24 +1,46 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
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 the tag starts with v, treat this as a official release
|
||||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||||
_docker_repo=${2:-grafana/grafana}
|
|
||||||
else
|
else
|
||||||
_grafana_version=$_grafana_tag
|
_grafana_version=$_grafana_tag
|
||||||
_docker_repo=${2:-grafana/grafana-dev}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
|
|
||||||
echo "pushing ${_docker_repo}:${_grafana_version}"
|
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
|
if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
|
||||||
echo "pushing ${_docker_repo}:latest"
|
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
|
elif echo "$_grafana_tag" | grep -q "master"; then
|
||||||
echo "pushing grafana/grafana:master"
|
docker_push_all "${_docker_repo}" "master"
|
||||||
docker push grafana/grafana:master
|
docker push "grafana/grafana-dev:${_grafana_version}"
|
||||||
fi
|
fi
|
||||||
|
@ -212,6 +212,10 @@ func GetAlertNotificationByID(c *m.ReqContext) Response {
|
|||||||
return Error(500, "Failed to get alert notifications", err)
|
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))
|
return JSON(200, dtos.NewAlertNotification(query.Result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +119,12 @@ func TestAlertingApiEndpoint(t *testing.T) {
|
|||||||
So(getAlertsQuery.Limit, ShouldEqual, 5)
|
So(getAlertsQuery.Limit, ShouldEqual, 5)
|
||||||
So(getAlertsQuery.Query, ShouldEqual, "alertQuery")
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard {
|
|||||||
func NewDashboardFolder(title string) *Dashboard {
|
func NewDashboardFolder(title string) *Dashboard {
|
||||||
folder := NewDashboard(title)
|
folder := NewDashboard(title)
|
||||||
folder.IsFolder = true
|
folder.IsFolder = true
|
||||||
folder.Data.Set("schemaVersion", 16)
|
folder.Data.Set("schemaVersion", 17)
|
||||||
folder.Data.Set("version", 0)
|
folder.Data.Set("version", 0)
|
||||||
folder.IsFolder = true
|
folder.IsFolder = true
|
||||||
return folder
|
return folder
|
||||||
|
@ -112,7 +112,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
|||||||
|
|
||||||
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
|
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ValidationError{Reason: "Could not parse frequency"}
|
return nil, ValidationError{Reason: err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
rawFor := jsonAlert.Get("for").MustString()
|
rawFor := jsonAlert.Get("for").MustString()
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
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 {
|
type Rule struct {
|
||||||
Id int64
|
Id int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
|
|||||||
matches := ValueFormatRegex.FindAllString(str, 1)
|
matches := ValueFormatRegex.FindAllString(str, 1)
|
||||||
|
|
||||||
if len(matches) <= 0 {
|
if len(matches) <= 0 {
|
||||||
return 0, fmt.Errorf("Frequency could not be parsed")
|
return 0, ErrFrequencyCouldNotBeParsed
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := strconv.Atoi(matches[0])
|
value, err := strconv.Atoi(matches[0])
|
||||||
@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if value == 0 {
|
||||||
|
return 0, ErrFrequencyCannotBeZeroOrLess
|
||||||
|
}
|
||||||
|
|
||||||
unit := UnitFormatRegex.FindAllString(str, 1)[0]
|
unit := UnitFormatRegex.FindAllString(str, 1)[0]
|
||||||
|
|
||||||
if val, ok := unitMultiplier[unit]; ok {
|
if val, ok := unitMultiplier[unit]; ok {
|
||||||
@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
|||||||
model.PanelId = ruleDef.PanelId
|
model.PanelId = ruleDef.PanelId
|
||||||
model.Name = ruleDef.Name
|
model.Name = ruleDef.Name
|
||||||
model.Message = ruleDef.Message
|
model.Message = ruleDef.Message
|
||||||
model.Frequency = ruleDef.Frequency
|
|
||||||
model.State = ruleDef.State
|
model.State = ruleDef.State
|
||||||
model.LastStateChange = ruleDef.NewStateDate
|
model.LastStateChange = ruleDef.NewStateDate
|
||||||
model.For = ruleDef.For
|
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.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
|
||||||
model.StateChanges = ruleDef.StateChanges
|
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() {
|
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||||
jsonModel := simplejson.NewFromAny(v)
|
jsonModel := simplejson.NewFromAny(v)
|
||||||
id, err := jsonModel.Get("id").Int64()
|
id, err := jsonModel.Get("id").Int64()
|
||||||
|
@ -14,6 +14,36 @@ func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
|
|||||||
return &ConditionResult{}, nil
|
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) {
|
func TestAlertRuleModel(t *testing.T) {
|
||||||
Convey("Testing alert rule", t, func() {
|
Convey("Testing alert rule", t, func() {
|
||||||
|
|
||||||
@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
return &FakeCondition{}, nil
|
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() {
|
Convey("should return err for empty string", func() {
|
||||||
_, err := getTimeDurationStringToSeconds("")
|
_, err := getTimeDurationStringToSeconds("")
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
So(len(alertRule.Notifications), ShouldEqual, 2)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package notifications
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -26,6 +27,9 @@ type Webhook struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var netTransport = &http.Transport{
|
var netTransport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Renegotiation: tls.RenegotiateFreelyAsClient,
|
||||||
|
},
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
Dial: (&net.Dialer{
|
Dial: (&net.Dialer{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
|
@ -6,6 +6,7 @@ import { SearchResult } from './components/search/SearchResult';
|
|||||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||||
|
import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||||
@ -19,4 +20,13 @@ export function registerAngularDirectives() {
|
|||||||
['onChange', { watchDepth: 'reference' }],
|
['onChange', { watchDepth: 'reference' }],
|
||||||
['tagOptions', { watchDepth: 'reference' }],
|
['tagOptions', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
|
react2AngularDirective('colorPicker', ColorPicker, [
|
||||||
|
'color',
|
||||||
|
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||||
|
]);
|
||||||
|
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
|
||||||
|
'series',
|
||||||
|
'onColorChange',
|
||||||
|
'onToggleAxis',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { SFC, ReactNode } from 'react';
|
import React, { SFC, ReactNode } from 'react';
|
||||||
import Tooltip from '../Tooltip/Tooltip';
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
@ -14,8 +14,10 @@ export const Label: SFC<Props> = props => {
|
|||||||
<span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
|
<span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
|
||||||
<span>{props.children}</span>
|
<span>{props.children}</span>
|
||||||
{props.tooltip && (
|
{props.tooltip && (
|
||||||
<Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
|
<Tooltip placement="auto" content={props.tooltip}>
|
||||||
|
<div className="gf-form-help-icon--right-normal">
|
||||||
<i className="gicon gicon-question gicon--has-hover" />
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
||||||
import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker';
|
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 { User } from 'app/types';
|
||||||
import {
|
import {
|
||||||
dashboardPermissionLevels,
|
dashboardPermissionLevels,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Select from 'app/core/components/Select/Select';
|
import { Select } from '@grafana/ui';
|
||||||
import { dashboardPermissionLevels } from 'app/types/acl';
|
import { dashboardPermissionLevels } from 'app/types/acl';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
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 { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
|
||||||
import { FolderInfo } from 'app/types';
|
import { FolderInfo } from 'app/types';
|
||||||
|
|
||||||
|
@ -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<Props, any> {
|
|
||||||
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 (
|
|
||||||
<div className="baron baron__root baron__clipper">
|
|
||||||
<div className={this.props.className + ' baron__scroller'} ref={this.handleRef}>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="baron__track">
|
|
||||||
<div className="baron__bar" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Select from './Select';
|
import { Select } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataSourceSelectItem } from 'app/types';
|
import { DataSourceSelectItem } from 'app/types';
|
||||||
|
@ -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: () => ({}),
|
|
||||||
};
|
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AsyncSelect } from './Select';
|
import { AsyncSelect } from '@grafana/ui';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import Select from './Select';
|
import { getValueFormats } from '@grafana/ui';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import { Select } from '@grafana/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onChange: (item: any) => void;
|
onChange: (item: any) => void;
|
||||||
@ -16,7 +16,7 @@ export default class UnitPicker extends PureComponent<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const { defaultValue, onChange, width } = this.props;
|
const { defaultValue, onChange, width } = this.props;
|
||||||
|
|
||||||
const unitGroups = kbn.getUnitFormats();
|
const unitGroups = getValueFormats();
|
||||||
|
|
||||||
// Need to transform the data structure to work well with Select
|
// Need to transform the data structure to work well with Select
|
||||||
const groupOptions = unitGroups.map(group => {
|
const groupOptions = unitGroups.map(group => {
|
||||||
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { AsyncSelect } from './Select';
|
import { AsyncSelect } from '@grafana/ui';
|
||||||
|
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import { Label } from 'app/core/components/Label/Label';
|
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 { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
import { DashboardSearchHit } from 'app/types';
|
import { DashboardSearchHit } from 'app/types';
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
|
||||||
import AsyncSelect from '@torkelo/react-select/lib/Async';
|
import AsyncSelect from '@torkelo/react-select/lib/Async';
|
||||||
|
|
||||||
import { TagOption } from './TagOption';
|
import { TagOption } from './TagOption';
|
||||||
import { TagBadge } from './TagBadge';
|
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 { components } from '@torkelo/react-select';
|
||||||
import ResetStyles from 'app/core/components/Select/ResetStyles';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
@ -51,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> {
|
|||||||
getOptionValue: i => i.value,
|
getOptionValue: i => i.value,
|
||||||
getOptionLabel: i => i.label,
|
getOptionLabel: i => i.label,
|
||||||
value: tags,
|
value: tags,
|
||||||
styles: ResetStyles,
|
styles: resetSelectStyles(),
|
||||||
filterOption: (option, searchQuery) => {
|
filterOption: (option, searchQuery) => {
|
||||||
const regex = RegExp(searchQuery, 'i');
|
const regex = RegExp(searchQuery, 'i');
|
||||||
return regex.test(option.value);
|
return regex.test(option.value);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { SFC, ReactNode, PureComponent } from 'react';
|
import React, { SFC, ReactNode, PureComponent } from 'react';
|
||||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
interface ToggleButtonGroupProps {
|
interface ToggleButtonGroupProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
@ -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(
|
|
||||||
<Popover className="test-class" placement="auto" content="Popover text">
|
|
||||||
<button>Button with Popover</button>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,19 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import Popper from './Popper';
|
|
||||||
import withPopper, { UsingPopperProps } from './withPopper';
|
|
||||||
|
|
||||||
class Popover extends PureComponent<UsingPopperProps> {
|
|
||||||
render() {
|
|
||||||
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
|
|
||||||
|
|
||||||
const togglePopper = restProps.show ? hidePopper : showPopper;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`popper__manager ${className}`} onClick={togglePopper}>
|
|
||||||
<Popper {...restProps}>{children}</Popper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withPopper(Popover);
|
|
@ -1,17 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import Popper from './Popper';
|
|
||||||
import withPopper, { UsingPopperProps } from './withPopper';
|
|
||||||
|
|
||||||
class Tooltip extends PureComponent<UsingPopperProps> {
|
|
||||||
render() {
|
|
||||||
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
|
|
||||||
<Popper {...restProps}>{children}</Popper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withPopper(Tooltip);
|
|
@ -1,16 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Popover renders correctly 1`] = `
|
|
||||||
<div
|
|
||||||
className="popper__manager test-class"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="popper_ref "
|
|
||||||
>
|
|
||||||
<button>
|
|
||||||
Button with Popover
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -1,19 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Tooltip renders correctly 1`] = `
|
|
||||||
<div
|
|
||||||
className="popper__manager test-class"
|
|
||||||
onMouseEnter={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="popper_ref "
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="http://www.grafana.com"
|
|
||||||
>
|
|
||||||
Link with tooltip
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -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<Props, State> {
|
|
||||||
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 <ReactComponent />;
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { show, placement } = this.state;
|
|
||||||
const className = this.props.className || '';
|
|
||||||
return (
|
|
||||||
<WrappedComponent
|
|
||||||
{...this.props}
|
|
||||||
showPopper={this.showPopper}
|
|
||||||
hidePopper={this.hidePopper}
|
|
||||||
renderContent={this.renderContent}
|
|
||||||
show={show}
|
|
||||||
placement={placement}
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -13,11 +13,10 @@ import './partials';
|
|||||||
import './components/jsontree/jsontree';
|
import './components/jsontree/jsontree';
|
||||||
import './components/code_editor/code_editor';
|
import './components/code_editor/code_editor';
|
||||||
import './utils/outline';
|
import './utils/outline';
|
||||||
import './components/colorpicker/ColorPicker';
|
|
||||||
import './components/colorpicker/SeriesColorPickerPopover';
|
|
||||||
import './components/colorpicker/spectrum_picker';
|
import './components/colorpicker/spectrum_picker';
|
||||||
import './services/search_srv';
|
import './services/search_srv';
|
||||||
import './services/ng_react';
|
import './services/ng_react';
|
||||||
|
import { colors } from '@grafana/ui/';
|
||||||
|
|
||||||
import { searchDirective } from './components/search/search';
|
import { searchDirective } from './components/search/search';
|
||||||
import { infoPopover } from './components/info_popover';
|
import { infoPopover } from './components/info_popover';
|
||||||
@ -36,7 +35,6 @@ import 'app/core/services/all';
|
|||||||
import './filters/filters';
|
import './filters/filters';
|
||||||
import coreModule from './core_module';
|
import coreModule from './core_module';
|
||||||
import appEvents from './app_events';
|
import appEvents from './app_events';
|
||||||
import colors from './utils/colors';
|
|
||||||
import { assignModelProperties } from './utils/model_utils';
|
import { assignModelProperties } from './utils/model_utils';
|
||||||
import { contextSrv } from './services/context_srv';
|
import { contextSrv } from './services/context_srv';
|
||||||
import { KeybindingSrv } from './services/keybindingSrv';
|
import { KeybindingSrv } from './services/keybindingSrv';
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { colors } from '@grafana/ui';
|
||||||
|
|
||||||
import { TimeSeries } from 'app/core/core';
|
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.
|
* Mapping of log level abbreviation to canonical log level.
|
||||||
|
8
public/app/core/specs/factors.test.ts
Normal file
8
public/app/core/specs/factors.test.ts
Normal file
@ -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]);
|
||||||
|
});
|
||||||
|
});
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user