diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c99011..08211f3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -197,7 +197,7 @@ jobs: steps: - checkout - run: sudo pip install codespell - - run: codespell -S './.git*,./src/img*,./go.sum,yarn.lock' -L que --ignore-words=./.codespell_ignore + - run: codespell -S './.git*, ./src/img*, ./go.sum, ./yarn.lock' -L que --ignore-words=./.codespell_ignore workflows: version: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d76c30..e7e0c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,54 @@ # Change Log -All notable changes to this project will be documented in this file. +## [3.12.2] - 2020-05-28 +### Fixed +- Annotations feature doesn't work, [#964](https://github.com/alexanderzobnin/grafana-zabbix/issues/964) +- Alias variables do not work with direct DB connection enabled, [#965](https://github.com/alexanderzobnin/grafana-zabbix/issues/965) -The format is based on [Keep a Changelog](http://keepachangelog.com/) -and this project adheres to [Semantic Versioning](http://semver.org/). +## [3.12.1] - 2020-05-25 +### Fixed +- Problems: panel fails with error (cannot read property 'description' of undefined), [#954](https://github.com/alexanderzobnin/grafana-zabbix/issues/954) +- Problems: problem name filter doesn't work, [#962](https://github.com/alexanderzobnin/grafana-zabbix/issues/962) +- Problems: acknowledged filter doesn't work, [#961](https://github.com/alexanderzobnin/grafana-zabbix/issues/961) -## Unreleased +## [3.12.0] - 2020-05-21 +### Added +- Variables: able to query item values, [#417](https://github.com/alexanderzobnin/grafana-zabbix/issues/417) +- Functions: expose host, item, app to the alias functions, [#619](https://github.com/alexanderzobnin/grafana-zabbix/issues/619) +- Problems: navigate to Explore and show graphs for the problem, [#948](https://github.com/alexanderzobnin/grafana-zabbix/issues/948) +- Problems: able to show Problems/Recent problems/History, [#495](https://github.com/alexanderzobnin/grafana-zabbix/issues/495) +- Problems: icon with acknowledges count, [#946](https://github.com/alexanderzobnin/grafana-zabbix/issues/946) +- IT Services: support SLA intervals, [#885](https://github.com/alexanderzobnin/grafana-zabbix/issues/885) + +### Fixed +- Explore doesn't work with Zabbix datasource, [#888](https://github.com/alexanderzobnin/grafana-zabbix/issues/888) +- SLA value is incorrect, [#885](https://github.com/alexanderzobnin/grafana-zabbix/issues/885) +- Graph panel randomly shows no data, [#861](https://github.com/alexanderzobnin/grafana-zabbix/issues/861) +- Variables: unable to edit variables in Grafana 7.0.0, [#949](https://github.com/alexanderzobnin/grafana-zabbix/issues/949) +- Variables: wrong variable scope inside repeated rows, [#912](https://github.com/alexanderzobnin/grafana-zabbix/issues/912) +- Problems: resolve macros in URLs, [#190](https://github.com/alexanderzobnin/grafana-zabbix/issues/190) +- Problems: unable to acknowledge resolved problem, [#942](https://github.com/alexanderzobnin/grafana-zabbix/issues/942) +- Problems: resolved problems color and severity set to Not classified, [#909](https://github.com/alexanderzobnin/grafana-zabbix/issues/909) +- Problems: can't acknowledge alert in panel with a single problem, [#900](https://github.com/alexanderzobnin/grafana-zabbix/issues/900) +- Annotations: `ITEM.VALUE` behaves like `ITEM.LASTVALUE` in annotations, [#891](https://github.com/alexanderzobnin/grafana-zabbix/issues/891) +- Alert state on the panel (heart icon) doesn't work in Grafana 6.7, [#931](https://github.com/alexanderzobnin/grafana-zabbix/issues/931) +- Consolidated average is not accurate with direct DB connection, [#752](https://github.com/alexanderzobnin/grafana-zabbix/issues/752) + +### Changed +- Problems panel uses new `problem.get` API which is not compatible with Zabbix 3.x, [#495](https://github.com/alexanderzobnin/grafana-zabbix/issues/495) +- Problems panel is metrics panel now, problems query editor moved to the data source. +- Zabbix version is auto detected now, [#727](https://github.com/alexanderzobnin/grafana-zabbix/issues/727) + +## [3.11.0] - 2020-03-23 +### Added +- Improve variable query editor, [#705](https://github.com/alexanderzobnin/grafana-zabbix/issues/705) +- Transform/percentile function, [#868](https://github.com/alexanderzobnin/grafana-zabbix/issues/868) + +### Fixed +- Problems panel: stopped working in Grafana 6.7.0, [#907](https://github.com/alexanderzobnin/grafana-zabbix/issues/907) +- Problems panel: event severity change, [#870](https://github.com/alexanderzobnin/grafana-zabbix/issues/870) +- Problems panel: color is changed to acknowledged even if there is only message without acknowledgment, [#857](https://github.com/alexanderzobnin/grafana-zabbix/issues/857) +- Percentile function returns incorrect results, [#862](https://github.com/alexanderzobnin/grafana-zabbix/issues/862) ## [3.10.5] - 2019-12-26 ### Added diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e47ccd5..76a01fe 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -3,7 +3,7 @@ site_description: Documentation for Grafana-Zabbix, Zabbix monitoring system plu site_url: https://alexanderzobnin.github.io/grafana-zabbix/ repo_url: https://github.com/alexanderzobnin/grafana-zabbix/ edit_uri: blob/docs/docs/sources/ -copyright: Copyright © 2015-2019, Alexander Zobnin +copyright: Copyright © 2015-2020, Alexander Zobnin docs_dir: sources diff --git a/docs/sources/configuration/index.md b/docs/sources/configuration/index.md index 2958f53..ed2b249 100644 --- a/docs/sources/configuration/index.md +++ b/docs/sources/configuration/index.md @@ -58,7 +58,7 @@ Direct DB Connection allows plugin to use existing SQL data source for querying database. This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces amount of data transferred. -Read [how to configure](./sql_datasource) SQL data source in Grafana. +Read [how to configure](./direct_db_datasource) SQL data source in Grafana. - **Enable**: enable Direct DB Connection. - **Data Source**: Select Data Source for Zabbix history database. diff --git a/docs/sources/installation/index.md b/docs/sources/installation/index.md index 606b9ae..c245d89 100644 --- a/docs/sources/installation/index.md +++ b/docs/sources/installation/index.md @@ -18,12 +18,12 @@ grafana-cli plugins install alexanderzobnin-zabbix-app Restart grafana after installing plugins ```sh -service grafana-server restart +systemctl restart grafana-server ``` -Read more about installing plugins in [Grafana docs](http://docs.grafana.org/plugins/installation/) +Read more about installing plugins in [Grafana docs](https://grafana.com/docs/plugins/installation/) -**WARNING!** The only reliable installation method is `grafana-cli`. Any other ways should be treated as a workaround an don't provide any backward-compatibulity guaranties. +**WARNING!** The only reliable installation method is `grafana-cli`. Any other way should be treated as a workaround and doesn't provide any backward-compatibility guaranties. ## From github repo **WARNING!** This way doesn't work anymore (`dist/` folder was removed from git). Use `grafana-cli` or build plugin from sources. diff --git a/docs/sources/reference/functions.md b/docs/sources/reference/functions.md index 22622bc..c2f1bb3 100644 --- a/docs/sources/reference/functions.md +++ b/docs/sources/reference/functions.md @@ -269,6 +269,20 @@ timeShift(+1d) - shift metric forward in 1 day ## Alias +Following template variables available for using in `setAlias()` and `replaceAlias()` functions: + +- `$__zbx_item`, `$__zbx_item_name` - item name +- `$__zbx_item_key` - item key +- `$__zbx_host_name` - visible name of the host +- `$__zbx_host` - technical name of the host + +Examples: +``` +setAlias($__zbx_host_name: $__zbx_item) -> backend01: CPU user time +setAlias(Item key: $__zbx_item_key) -> Item key: system.cpu.load[percpu,avg1] +setAlias($__zbx_host_name) -> backend01 +``` + ### _setAlias_ ``` setAlias(alias) @@ -310,7 +324,7 @@ Replace metric name using pattern. Pattern is regex or regular string. If regex |$' | Inserts the portion of the string that follows the matched substring. | |$n | Where n is a non-negative integer less than 100, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object. | -For more detais see [String.prototype.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) function. +For more details see [String.prototype.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) function. Examples: ``` diff --git a/package.json b/package.json index 5415f2a..f35b4c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "grafana-zabbix", "private": false, - "version": "3.10.5", + "version": "3.11.0", "description": "Zabbix plugin for Grafana", "homepage": "http://grafana-zabbix.org", "scripts": { @@ -30,10 +30,11 @@ "@babel/preset-env": "^7.8.3", "@babel/preset-react": "^7.8.3", "@emotion/core": "^10.0.27", - "@grafana/data": "canary", - "@grafana/runtime": "canary", - "@grafana/toolkit": "canary", - "@grafana/ui": "canary", + "@grafana/data": "^6.7.3", + "@grafana/runtime": "^6.7.3", + "@grafana/ui": "^6.7.3", + "@grafana/toolkit": "^6.7.3", + "@popperjs/core": "^2.4.0", "@types/classnames": "^2.2.9", "@types/grafana": "github:CorpGlory/types-grafana", "@types/jest": "^23.1.1", @@ -69,24 +70,26 @@ "jshint-stylish": "^2.1.0", "load-grunt-tasks": "~3.2.0", "lodash": "~4.17.13", + "memoize-one": "^5.1.1", "moment": "~2.21.0", "ng-annotate-webpack-plugin": "^0.3.0", "node-sass": "^4.13.0", "prop-types": "^15.6.2", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "react-popper": "^1.3.2", - "react-table": "^6.8.6", + "react": "16.12.0", + "react-dom": "16.12.0", + "react-popper": "^2.2.3", + "react-table-6": "^6.8.6", "react-test-renderer": "^16.7.0", "react-transition-group": "^2.5.2", "rst2html": "github:thoward/rst2html#990cb89", "sass-loader": "^8.0.0", + "semver": "^7.3.2", "style-loader": "^0.23.1", "tether-drop": "^1.4.2", "ts-jest": "^24.2.0", "ts-loader": "^6.2.0", "tslint": "5.20.1", - "typescript": "3.7.2", + "typescript": "^3.9.2", "webpack": "4.29.6", "webpack-cli": "3.2.3" }, diff --git a/src/components/config.html b/src/app_config_ctrl/config.html similarity index 100% rename from src/components/config.html rename to src/app_config_ctrl/config.html diff --git a/src/app_config_ctrl/config.js b/src/app_config_ctrl/config.js new file mode 100644 index 0000000..b23554a --- /dev/null +++ b/src/app_config_ctrl/config.js @@ -0,0 +1,4 @@ +export class ZabbixAppConfigCtrl { + constructor() { } +} +ZabbixAppConfigCtrl.templateUrl = 'app_config_ctrl/config.html'; diff --git a/src/components/AckButton/AckButton.tsx b/src/components/AckButton/AckButton.tsx new file mode 100644 index 0000000..e5d6f96 --- /dev/null +++ b/src/components/AckButton/AckButton.tsx @@ -0,0 +1,13 @@ +import React, { FC } from 'react'; +import { ActionButton } from '../ActionButton/ActionButton'; + +interface Props { + className?: string; + onClick(): void; +} + +export const AckButton: FC = ({ className, onClick }) => { + return ( + + ); +}; diff --git a/src/components/ActionButton/ActionButton.tsx b/src/components/ActionButton/ActionButton.tsx new file mode 100644 index 0000000..40fa697 --- /dev/null +++ b/src/components/ActionButton/ActionButton.tsx @@ -0,0 +1,71 @@ +import React, { FC } from 'react'; +import { cx, css } from 'emotion'; +import { stylesFactory, useTheme } from '@grafana/ui'; +import { GrafanaTheme, GrafanaThemeType } from '@grafana/data'; +import { FAIcon } from '../FAIcon/FAIcon'; +import { Tooltip } from '../Tooltip/Tooltip'; + +interface Props { + icon?: string; + width?: number; + tooltip?: string; + className?: string; + onClick(event: React.MouseEvent): void; +} + +export const ActionButton: FC = ({ icon, width, tooltip, className, children, onClick }) => { + const theme = useTheme(); + const styles = getStyles(theme); + const buttonClass = cx( + 'btn', + styles.button, + css`width: ${width || 3}rem`, + className, + ); + + let button = ( + + ); + + if (tooltip) { + button = ( + + {button} + + ); + } + + return button; +}; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const actionBlue = theme.type === GrafanaThemeType.Light ? '#497dc0' : '#005f81'; + const hoverBlue = theme.type === GrafanaThemeType.Light ? '#456ba4' : '#354f77'; + + return { + button: css` + height: 2rem; + background-image: none; + background-color: ${actionBlue}; + border: 1px solid ${theme.colors.gray1 || (theme as any).palette.gray1}; + border-radius: 1px; + color: ${theme.colors.text}; + + i { + vertical-align: middle; + } + + &:hover { + background-color: ${hoverBlue}; + } + `, + icon: css` + i { + color: ${theme.colors.text}; + } + `, + }; +}); diff --git a/src/components/ConfigProvider/ConfigProvider.tsx b/src/components/ConfigProvider/ConfigProvider.tsx new file mode 100644 index 0000000..58c4705 --- /dev/null +++ b/src/components/ConfigProvider/ConfigProvider.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { config, GrafanaBootConfig } from '@grafana/runtime'; +import { ThemeContext, getTheme } from '@grafana/ui'; +import { GrafanaThemeType } from '@grafana/data'; + +export const ConfigContext = React.createContext(config); +export const ConfigConsumer = ConfigContext.Consumer; + +export const provideConfig = (component: React.ComponentType) => { + const ConfigProvider = (props: any) => ( + {React.createElement(component, { ...props })} + ); + + return ConfigProvider; +}; + +export const getCurrentThemeName = () => + config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark; + +export const getCurrentTheme = () => getTheme(getCurrentThemeName()); + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + return ( + + {config => { + return {children}; + }} + + ); +}; + +export const provideTheme = (component: React.ComponentType) => { + return provideConfig((props: any) => {React.createElement(component, { ...props })}); +}; diff --git a/src/components/ExploreButton/ExploreButton.tsx b/src/components/ExploreButton/ExploreButton.tsx new file mode 100644 index 0000000..2396910 --- /dev/null +++ b/src/components/ExploreButton/ExploreButton.tsx @@ -0,0 +1,54 @@ +import React, { FC } from 'react'; +import { getLocationSrv } from '@grafana/runtime'; +import { MODE_METRICS, MODE_ITEMID } from '../../datasource-zabbix/constants'; +import { renderUrl } from '../../panel-triggers/utils'; +import { expandItemName } from '../../datasource-zabbix/utils'; +import { ProblemDTO } from '../../datasource-zabbix/types'; +import { ActionButton } from '../ActionButton/ActionButton'; + +interface Props { + problem: ProblemDTO; + panelId: number; +} + +export const ExploreButton: FC = ({ problem, panelId }) => { + return ( + openInExplore(problem, panelId)}> + Explore + + ); +}; + +const openInExplore = (problem: ProblemDTO, panelId: number) => { + let query: any = {}; + + if (problem.items?.length === 1 && problem.hosts?.length === 1) { + const item = problem.items[0]; + const host = problem.hosts[0]; + const itemName = expandItemName(item.name, item.key_); + query = { + queryType: MODE_METRICS, + group: { filter: '/.*/' }, + application: { filter: '' }, + host: { filter: host.name }, + item: { filter: itemName }, + }; + } else { + const itemids = problem.items?.map(p => p.itemid).join(','); + query = { + queryType: MODE_ITEMID, + itemids: itemids, + }; + } + + const state: any = { + datasource: problem.datasource, + context: 'explore', + originPanelId: panelId, + queries: [query], + }; + + const exploreState = JSON.stringify(state); + const url = renderUrl('/explore', { left: exploreState }); + getLocationSrv().update({ path: url, query: {} }); +}; diff --git a/src/components/FAIcon/FAIcon.tsx b/src/components/FAIcon/FAIcon.tsx new file mode 100644 index 0000000..04e03d1 --- /dev/null +++ b/src/components/FAIcon/FAIcon.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react'; +import { cx } from 'emotion'; + +interface Props { + icon: string; + customClass?: string; +} + +export const FAIcon: FC = ({ icon, customClass }) => { + const wrapperClass = cx(customClass, 'fa-icon-container'); + + return ( + + + + ); +}; + +export default FAIcon; diff --git a/src/components/GFHeartIcon/GFHeartIcon.tsx b/src/components/GFHeartIcon/GFHeartIcon.tsx new file mode 100644 index 0000000..4a67f2c --- /dev/null +++ b/src/components/GFHeartIcon/GFHeartIcon.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import { cx } from 'emotion'; + +interface Props { + status: 'critical' | 'warning' | 'online' | 'ok' | 'problem'; + className?: string; +} + +export const GFHeartIcon: FC = ({ status, className }) => { + const iconClass = cx( + className, + 'icon-gf', + { "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'}, + { "icon-gf-online": status === 'online' || status === 'ok' }, + ); + + return ( + + ); +}; + +export default GFHeartIcon; diff --git a/src/components/Modal/ModalController.tsx b/src/components/Modal/ModalController.tsx new file mode 100644 index 0000000..0d944f1 --- /dev/null +++ b/src/components/Modal/ModalController.tsx @@ -0,0 +1,70 @@ +import React, { FC } from 'react'; +import ReactDOM from 'react-dom'; +import { provideTheme } from '../ConfigProvider/ConfigProvider'; + +interface ModalWrapperProps { + showModal: (component: React.ComponentType, props: T) => void; + hideModal: () => void; +} + +type ModalWrapper = FC; + +interface Props { + children: ModalWrapper; +} + +interface State { + component: React.ComponentType | null; + props: any; +} + +export class ModalController extends React.Component { + modalRoot = document.body; + modalNode = document.createElement('div'); + + constructor(props: Props) { + super(props); + this.state = { + component: null, + props: {}, + }; + } + + showModal = (component: React.ComponentType, props: any) => { + this.setState({ + component, + props + }); + }; + + hideModal = () => { + this.modalRoot.removeChild(this.modalNode); + this.setState({ + component: null, + props: {}, + }); + }; + + renderModal() { + const { component, props } = this.state; + if (!component) { + return null; + } + + this.modalRoot.appendChild(this.modalNode); + const modal = React.createElement(provideTheme(component), props); + return ReactDOM.createPortal(modal, this.modalNode); + } + + render() { + const { children } = this.props; + const ChildrenComponent = children; + + return ( + <> + + {this.renderModal()} + + ); + } +} diff --git a/src/components/Tooltip/Popper.tsx b/src/components/Tooltip/Popper.tsx new file mode 100644 index 0000000..3d492f5 --- /dev/null +++ b/src/components/Tooltip/Popper.tsx @@ -0,0 +1,75 @@ +import React, { FC } from 'react'; +import { cx, css } from 'emotion'; +import { Manager, Popper as ReactPopper, Reference } from 'react-popper'; +import Transition from 'react-transition-group/Transition'; +import { stylesFactory } from '@grafana/ui'; +import BodyPortal from './Portal'; + +const getStyles = stylesFactory(() => ({ + defaultTransitionStyles: css` + transition: opacity 200ms linear; + opacity: 0; + `, +})); + +const transitionStyles = { + exited: { opacity: 0 }, + entering: { opacity: 0 }, + entered: { opacity: 1 }, + exiting: { opacity: 0 }, +}; + +interface Props { + renderContent: (content: any) => any; + show: boolean; + placement?: any; + content: string | ((props: any) => JSX.Element); + refClassName?: string; + popperClassName?: string; +} + +const Popper: FC = ({ show, placement, popperClassName, refClassName, content, children, renderContent }) => { + const refClass = cx('popper_ref', refClassName); + const styles = getStyles(); + const popperClass = cx('popper', popperClassName, styles.defaultTransitionStyles); + + return ( + + + {({ ref }) => ( +
+ {children} +
+ )} +
+ + {transitionState => ( + + + {({ ref, style, placement, arrowProps }) => { + return ( +
+
+ {renderContent(content)} +
+
+
+ ); + }} + + + )} + + + ); +}; + +export default Popper; diff --git a/src/panel-triggers/components/Tooltip/Portal.tsx b/src/components/Tooltip/Portal.tsx similarity index 90% rename from src/panel-triggers/components/Tooltip/Portal.tsx rename to src/components/Tooltip/Portal.tsx index dd2bb3b..e5a4527 100644 --- a/src/panel-triggers/components/Tooltip/Portal.tsx +++ b/src/components/Tooltip/Portal.tsx @@ -7,7 +7,7 @@ interface Props { } export default class BodyPortal extends PureComponent { - node: HTMLElement = document.createElement('div'); + node: HTMLElement; portalRoot: HTMLElement; constructor(props) { @@ -17,6 +17,7 @@ export default class BodyPortal extends PureComponent { root = document.body } = this.props; + this.node = document.createElement('div'); if (className) { this.node.classList.add(className); } diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000..45cd911 --- /dev/null +++ b/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import Popper from './Popper'; +import withPopper, { UsingPopperProps } from './withPopper'; + +const TooltipWrapper: FC = ({ hidePopper, showPopper, className, children, ...restProps }) => { + return ( +
+ {children} +
+ ); +}; + +export const Tooltip = withPopper(TooltipWrapper); + +export default Tooltip; diff --git a/src/panel-triggers/components/Tooltip/withPopper.tsx b/src/components/Tooltip/withPopper.tsx similarity index 64% rename from src/panel-triggers/components/Tooltip/withPopper.tsx rename to src/components/Tooltip/withPopper.tsx index 00ec28b..b50c798 100644 --- a/src/panel-triggers/components/Tooltip/withPopper.tsx +++ b/src/components/Tooltip/withPopper.tsx @@ -21,44 +21,28 @@ interface Props { } interface State { - placement: string; show: boolean; } -export default function withPopper(WrappedComponent) { +export const withPopper = (WrappedComponent) => { return class extends React.Component { + static defaultProps: Partial = { + placement: 'auto', + }; + 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, - })); + this.setState({ show: true }); }; hidePopper = () => { - this.setState(prevState => ({ - ...prevState, - show: false, - })); + this.setState({ show: false }); }; renderContent(content) { @@ -71,8 +55,8 @@ export default function withPopper(WrappedComponent) { } render() { - const { show, placement } = this.state; - const className = this.props.className || ''; + const { show } = this.state; + const { placement, className } = this.props; return ( ); } }; -} +}; + +export default withPopper; diff --git a/src/components/config.js b/src/components/config.js deleted file mode 100644 index fe9c1ef..0000000 --- a/src/components/config.js +++ /dev/null @@ -1,4 +0,0 @@ -export class ZabbixAppConfigCtrl { - constructor() { } -} -ZabbixAppConfigCtrl.templateUrl = 'components/config.html'; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..767e5b0 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,6 @@ +export { GFHeartIcon } from './GFHeartIcon/GFHeartIcon'; +export { FAIcon } from './FAIcon/FAIcon'; +export { AckButton } from './AckButton/AckButton'; +export { ExploreButton } from './ExploreButton/ExploreButton'; +export { Tooltip } from './Tooltip/Tooltip'; +export { ModalController } from './Modal/ModalController'; diff --git a/src/datasource-zabbix/components/FunctionEditor.tsx b/src/datasource-zabbix/components/FunctionEditor.tsx index c6ae673..528fc05 100644 --- a/src/datasource-zabbix/components/FunctionEditor.tsx +++ b/src/datasource-zabbix/components/FunctionEditor.tsx @@ -39,7 +39,6 @@ class FunctionEditor extends React.PureComponent

{name}

{description}
- />
); } diff --git a/src/datasource-zabbix/components/VariableQueryEditor.tsx b/src/datasource-zabbix/components/VariableQueryEditor.tsx index a9b5616..d70d551 100644 --- a/src/datasource-zabbix/components/VariableQueryEditor.tsx +++ b/src/datasource-zabbix/components/VariableQueryEditor.tsx @@ -1,16 +1,22 @@ import React, { PureComponent } from 'react'; import { parseLegacyVariableQuery } from '../utils'; -import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types'; import { ZabbixInput } from './ZabbixInput'; +// FormLabel was renamed to InlineFormLabel in Grafana 7.0 +import * as grafanaUi from '@grafana/ui'; +const FormLabel = grafanaUi.FormLabel || (grafanaUi as any).InlineFormLabel; +const Select = (grafanaUi as any).LegacyForms?.Select || (grafanaUi as any).Select; +const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input; + export class ZabbixVariableQueryEditor extends PureComponent { queryTypes: Array> = [ { value: VariableQueryTypes.Group, label: 'Group'}, { value: VariableQueryTypes.Host, label: 'Host' }, { value: VariableQueryTypes.Application, label: 'Application' }, { value: VariableQueryTypes.Item, label: 'Item' }, + { value: VariableQueryTypes.ItemValues, label: 'Item values' }, ]; defaults: VariableQueryData = { @@ -119,7 +125,8 @@ export class ZabbixVariableQueryEditor extends PureComponent {(selectedQueryType.value === VariableQueryTypes.Application || - selectedQueryType.value === VariableQueryTypes.Item) && + selectedQueryType.value === VariableQueryTypes.Item || + selectedQueryType.value === VariableQueryTypes.ItemValues) &&
Application @@ -129,7 +136,8 @@ export class ZabbixVariableQueryEditor extends PureComponent
- {selectedQueryType.value === VariableQueryTypes.Item && + {(selectedQueryType.value === VariableQueryTypes.Item || + selectedQueryType.value === VariableQueryTypes.ItemValues) &&
Item ({ inputRegex: css` - color: ${theme.colors.orange} + color: ${theme.colors.orange || (theme as any).palette.orange} `, inputVariable: css` - color: ${theme.colors.variable} + color: ${theme.colors.variable || (theme as any).palette.variable} `, }); @@ -43,17 +46,14 @@ const zabbixInputValidationEvents: ValidationEvents = { ], }; -interface Props extends React.ComponentProps, Themeable { -} - -const UnthemedZabbixInput: FC = ({ theme, value, ref, validationEvents, ...restProps }) => { +export const ZabbixInput: FC = ({ value, ref, validationEvents, ...restProps }) => { + const theme = useTheme(); const styles = getStyles(theme); - let inputClass; + let inputClass = styles.inputRegex; if (variablePattern.test(value as string)) { inputClass = styles.inputVariable; - } - if (isRegex(value)) { + } else if (isRegex(value)) { inputClass = styles.inputRegex; } @@ -66,5 +66,3 @@ const UnthemedZabbixInput: FC = ({ theme, value, ref, validationEvents, . /> ); }; - -export const ZabbixInput = withTheme(UnthemedZabbixInput); diff --git a/src/datasource-zabbix/config.controller.js b/src/datasource-zabbix/config.controller.js index 926d5b0..c160d29 100644 --- a/src/datasource-zabbix/config.controller.js +++ b/src/datasource-zabbix/config.controller.js @@ -1,14 +1,9 @@ import _ from 'lodash'; +import { getDataSourceSrv } from '@grafana/runtime'; import { migrateDSConfig } from './migrations'; const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb']; -const zabbixVersions = [ - { name: '2.x', value: 2 }, - { name: '3.x', value: 3 }, - { name: '4.x', value: 4 }, -]; - const defaultConfig = { trends: false, dbConnectionEnable: false, @@ -17,29 +12,24 @@ const defaultConfig = { addThresholds: false, alertingMinSeverity: 3, disableReadOnlyUsersAck: false, - zabbixVersion: 3, }; export class ZabbixDSConfigController { /** @ngInject */ - constructor($scope, $injector, datasourceSrv) { - this.datasourceSrv = datasourceSrv; - + constructor() { this.current.jsonData = migrateDSConfig(this.current.jsonData); _.defaults(this.current.jsonData, defaultConfig); this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId; this.dbDataSources = this.getSupportedDBDataSources(); - this.zabbixVersions = _.cloneDeep(zabbixVersions); - this.autoDetectZabbixVersion(); if (!this.dbConnectionDatasourceId) { this.loadCurrentDBDatasource(); } } getSupportedDBDataSources() { - let datasources = this.datasourceSrv.getAll(); + let datasources = getDataSourceSrv().getAll(); return _.filter(datasources, ds => { return _.includes(SUPPORTED_SQL_DS, ds.type); }); @@ -53,7 +43,7 @@ export class ZabbixDSConfigController { loadCurrentDBDatasource() { const dsName= this.current.jsonData.dbConnectionDatasourceName; - this.datasourceSrv.loadDatasource(dsName) + getDataSourceSrv().loadDatasource(dsName) .then(ds => { if (ds) { this.dbConnectionDatasourceId = ds.id; @@ -61,25 +51,6 @@ export class ZabbixDSConfigController { }); } - autoDetectZabbixVersion() { - if (!this.current.id) { - return; - } - - this.datasourceSrv.loadDatasource(this.current.name) - .then(ds => { - return ds.getVersion(); - }) - .then(version => { - if (version) { - if (!_.find(zabbixVersions, ['value', version])) { - this.zabbixVersions.push({ name: version + '.x', value: version }); - } - this.current.jsonData.zabbixVersion = version; - } - }); - } - onDBConnectionDatasourceChange() { this.current.jsonData.dbConnectionDatasourceId = this.dbConnectionDatasourceId; } diff --git a/src/datasource-zabbix/constants.js b/src/datasource-zabbix/constants.ts similarity index 76% rename from src/datasource-zabbix/constants.js rename to src/datasource-zabbix/constants.ts index 79d2edc..41d3776 100644 --- a/src/datasource-zabbix/constants.js +++ b/src/datasource-zabbix/constants.ts @@ -1,3 +1,7 @@ +// Plugin IDs +export const ZABBIX_PROBLEMS_PANEL_ID = 'alexanderzobnin-zabbix-triggers-panel'; +export const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource'; + // Data point export const DATAPOINT_VALUE = 0; export const DATAPOINT_TS = 1; @@ -8,6 +12,7 @@ export const MODE_ITSERVICE = 1; export const MODE_TEXT = 2; export const MODE_ITEMID = 3; export const MODE_TRIGGERS = 4; +export const MODE_PROBLEMS = 5; // Triggers severity export const SEV_NOT_CLASSIFIED = 0; @@ -23,8 +28,10 @@ export const SHOW_OK_EVENTS = 1; // Acknowledge export const ZBX_ACK_ACTION_NONE = 0; +export const ZBX_ACK_ACTION_CLOSE = 1; export const ZBX_ACK_ACTION_ACK = 2; export const ZBX_ACK_ACTION_ADD_MESSAGE = 4; +export const ZBX_ACK_ACTION_CHANGE_SEVERITY = 8; export const TRIGGER_SEVERITY = [ {val: 0, text: 'Not classified'}, @@ -39,3 +46,5 @@ export const TRIGGER_SEVERITY = [ export const MIN_SLA_INTERVAL = 3600; export const RANGE_VARIABLE_VALUE = 'range_series'; + +export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001; diff --git a/src/datasource-zabbix/dataProcessor.js b/src/datasource-zabbix/dataProcessor.ts similarity index 65% rename from src/datasource-zabbix/dataProcessor.js rename to src/datasource-zabbix/dataProcessor.ts index a8ecdb6..5df0af0 100644 --- a/src/datasource-zabbix/dataProcessor.js +++ b/src/datasource-zabbix/dataProcessor.ts @@ -1,35 +1,37 @@ import _ from 'lodash'; +// Available in 7.0 +// import { getTemplateSrv } from '@grafana/runtime'; import * as utils from './utils'; import ts, { groupBy_perf as groupBy } from './timeseries'; -let SUM = ts.SUM; -let COUNT = ts.COUNT; -let AVERAGE = ts.AVERAGE; -let MIN = ts.MIN; -let MAX = ts.MAX; -let MEDIAN = ts.MEDIAN; -let PERCENTILE = ts.PERCENTILE; +const SUM = ts.SUM; +const COUNT = ts.COUNT; +const AVERAGE = ts.AVERAGE; +const MIN = ts.MIN; +const MAX = ts.MAX; +const MEDIAN = ts.MEDIAN; +const PERCENTILE = ts.PERCENTILE; -let downsampleSeries = ts.downsample; -let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc); -let sumSeries = ts.sumSeries; -let delta = ts.delta; -let rate = ts.rate; -let scale = (factor, datapoints) => ts.scale_perf(datapoints, factor); -let offset = (delta, datapoints) => ts.offset(datapoints, delta); -let simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n); -let expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a); -let percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n)); +const downsampleSeries = ts.downsample; +const groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc); +const sumSeries = ts.sumSeries; +const delta = ts.delta; +const rate = ts.rate; +const scale = (factor, datapoints) => ts.scale_perf(datapoints, factor); +const offset = (delta, datapoints) => ts.offset(datapoints, delta); +const simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n); +const expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a); +const percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n)); function limit(order, n, orderByFunc, timeseries) { - let orderByCallback = aggregationFunctions[orderByFunc]; - let sortByIteratee = (ts) => { - let values = _.map(ts.datapoints, (point) => { + const orderByCallback = aggregationFunctions[orderByFunc]; + const sortByIteratee = (ts) => { + const values = _.map(ts.datapoints, (point) => { return point[0]; }); return orderByCallback(values); }; - let sortedTimeseries = _.sortBy(timeseries, sortByIteratee); + const sortedTimeseries = _.sortBy(timeseries, sortByIteratee); if (order === 'bottom') { return sortedTimeseries.slice(0, n); } else { @@ -64,13 +66,17 @@ function transformNull(n, datapoints) { }); } -function sortSeries(direction, timeseries) { - return _.orderBy(timeseries, [function (ts) { +function sortSeries(direction, timeseries: any[]) { + return _.orderBy(timeseries, [ts => { return ts.target.toLowerCase(); }], direction); } function setAlias(alias, timeseries) { + // TODO: use getTemplateSrv() when available (since 7.0) + if (this.templateSrv && timeseries && timeseries.scopedVars) { + alias = this.templateSrv.replace(alias, timeseries.scopedVars); + } timeseries.target = alias; return timeseries; } @@ -84,6 +90,10 @@ function replaceAlias(regexp, newAlias, timeseries) { } let alias = timeseries.target.replace(pattern, newAlias); + // TODO: use getTemplateSrv() when available (since 7.0) + if (this.templateSrv && timeseries && timeseries.scopedVars) { + alias = this.templateSrv.replace(alias, timeseries.scopedVars); + } timeseries.target = alias; return timeseries; } @@ -94,14 +104,13 @@ function setAliasByRegex(alias, timeseries) { } function extractText(str, pattern) { - var extractPattern = new RegExp(pattern); - var extractedValue = extractPattern.exec(str); - extractedValue = extractedValue[0]; - return extractedValue; + const extractPattern = new RegExp(pattern); + const extractedValue = extractPattern.exec(str); + return extractedValue[0]; } function groupByWrapper(interval, groupFunc, datapoints) { - var groupByCallback = aggregationFunctions[groupFunc]; + const groupByCallback = aggregationFunctions[groupFunc]; return groupBy(datapoints, interval, groupByCallback); } @@ -110,12 +119,12 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) { const flattenedPoints = ts.flattenDatapoints(datapoints); // groupBy_perf works with sorted series only const sortedPoints = ts.sortByTime(flattenedPoints); - let groupByCallback = aggregationFunctions[aggregateFunc]; + const groupByCallback = aggregationFunctions[aggregateFunc]; return groupBy(sortedPoints, interval, groupByCallback); } function aggregateWrapper(groupByCallback, interval, datapoints) { - var flattenedPoints = ts.flattenDatapoints(datapoints); + const flattenedPoints = ts.flattenDatapoints(datapoints); // groupBy_perf works with sorted series only const sortedPoints = ts.sortByTime(flattenedPoints); return groupBy(sortedPoints, interval, groupByCallback); @@ -125,19 +134,19 @@ function percentileAgg(interval, n, datapoints) { const flattenedPoints = ts.flattenDatapoints(datapoints); // groupBy_perf works with sorted series only const sortedPoints = ts.sortByTime(flattenedPoints); - let groupByCallback = _.partial(PERCENTILE, n); + const groupByCallback = _.partial(PERCENTILE, n); return groupBy(sortedPoints, interval, groupByCallback); } function timeShift(interval, range) { - let shift = utils.parseTimeShiftInterval(interval) / 1000; + const shift = utils.parseTimeShiftInterval(interval) / 1000; return _.map(range, time => { return time - shift; }); } function unShiftTimeSeries(interval, datapoints) { - let unshift = utils.parseTimeShiftInterval(interval); + const unshift = utils.parseTimeShiftInterval(interval); return _.map(datapoints, dp => { return [ dp[0], @@ -146,7 +155,7 @@ function unShiftTimeSeries(interval, datapoints) { }); } -let metricFunctions = { +const metricFunctions = { groupBy: groupByWrapper, scale: scale, offset: offset, @@ -177,7 +186,7 @@ let metricFunctions = { replaceAlias: replaceAlias }; -let aggregationFunctions = { +const aggregationFunctions = { avg: AVERAGE, min: MIN, max: MAX, diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.ts similarity index 64% rename from src/datasource-zabbix/datasource.js rename to src/datasource-zabbix/datasource.ts index 87db0b7..6746ad1 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import config from 'grafana/app/core/config'; +import { contextSrv } from 'grafana/app/core/core'; import * as dateMath from 'grafana/app/core/utils/datemath'; import * as utils from './utils'; import * as migrations from './migrations'; @@ -7,34 +8,44 @@ import * as metricFunctions from './metricFunctions'; import * as c from './constants'; import dataProcessor from './dataProcessor'; import responseHandler from './responseHandler'; +import problemsHandler from './problemsHandler'; import { Zabbix } from './zabbix/zabbix'; import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore'; -import { - DataSourceApi, - // DataSourceInstanceSettings, -} from '@grafana/data'; -// import { BackendSrv, DataSourceSrv } from '@grafana/runtime'; -// import { ZabbixAlertingService } from './zabbixAlerting.service'; -// import { ZabbixConnectionTestQuery, ZabbixConnectionInfo, TemplateSrv, TSDBResponse } from './types'; -import { VariableQueryTypes } from './types'; - -const DEFAULT_ZABBIX_VERSION = 3; +import { VariableQueryTypes, ShowProblemTypes } from './types'; +import { getBackendSrv } from '@grafana/runtime'; +import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; export class ZabbixDatasource extends DataSourceApi { + name: string; + url: string; + basicAuth: any; + withCredentials: any; - /** - * @ngInject - * @param {DataSourceInstanceSettings} instanceSettings - * @param {TemplateSrv} templateSrv - * @param {BackendSrv} backendSrv - * @param {DataSourceSrv} datasourceSrv - * @param {ZabbixAlertingService} zabbixAlertingSrv - */ - constructor(instanceSettings, templateSrv, backendSrv, datasourceSrv, zabbixAlertingSrv) { + username: string; + password: string; + trends: boolean; + trendsFrom: string; + trendsRange: string; + cacheTTL: any; + alertingEnabled: boolean; + addThresholds: boolean; + alertingMinSeverity: string; + disableReadOnlyUsersAck: boolean; + enableDirectDBConnection: boolean; + dbConnectionDatasourceId: number; + dbConnectionDatasourceName: string; + dbConnectionRetentionPolicy: string; + enableDebugLog: boolean; + datasourceId: number; + zabbix: Zabbix; + + replaceTemplateVars: (target: any, scopedVars?: any) => any; + + /** @ngInject */ + constructor(instanceSettings: DataSourceInstanceSettings, private templateSrv, private zabbixAlertingSrv) { super(instanceSettings); - this.type = 'zabbix'; + this.templateSrv = templateSrv; - this.backendSrv = backendSrv; this.zabbixAlertingSrv = zabbixAlertingSrv; this.enableDebugLog = config.buildInfo.env === 'development'; @@ -61,7 +72,7 @@ export class ZabbixDatasource extends DataSourceApi { this.trendsRange = jsonData.trendsRange || '4d'; // Set cache update interval - var ttl = jsonData.cacheTTL || '1h'; + const ttl = jsonData.cacheTTL || '1h'; this.cacheTTL = utils.parseInterval(ttl); // Alerting options @@ -71,7 +82,6 @@ export class ZabbixDatasource extends DataSourceApi { // Other options this.disableReadOnlyUsersAck = jsonData.disableReadOnlyUsersAck; - this.zabbixVersion = jsonData.zabbixVersion || DEFAULT_ZABBIX_VERSION; // Direct DB Connection options this.enableDirectDBConnection = jsonData.dbConnectionEnable || false; @@ -79,21 +89,21 @@ export class ZabbixDatasource extends DataSourceApi { this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName; this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy; - let zabbixOptions = { + const zabbixOptions = { url: this.url, username: this.username, password: this.password, basicAuth: this.basicAuth, withCredentials: this.withCredentials, - zabbixVersion: this.zabbixVersion, cacheTTL: this.cacheTTL, enableDirectDBConnection: this.enableDirectDBConnection, dbConnectionDatasourceId: this.dbConnectionDatasourceId, dbConnectionDatasourceName: this.dbConnectionDatasourceName, dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy, + datasourceId: this.datasourceId, }; - this.zabbix = new Zabbix(zabbixOptions, datasourceSrv, backendSrv, this.datasourceId); + this.zabbix = new Zabbix(zabbixOptions); } //////////////////////// @@ -124,7 +134,7 @@ export class ZabbixDatasource extends DataSourceApi { } // Create request for each target - let promises = _.map(options.targets, t => { + const promises = _.map(options.targets, t => { // Don't request for hidden targets if (t.hide) { return []; @@ -144,40 +154,45 @@ export class ZabbixDatasource extends DataSourceApi { this.replaceTargetVariables(target, options); // Apply Time-related functions (timeShift(), etc) - let timeFunctions = bindFunctionDefs(target.functions, 'Time'); + const timeFunctions = bindFunctionDefs(target.functions, 'Time'); if (timeFunctions.length) { const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]); timeFrom = time_from; timeTo = time_to; } - let timeRange = [timeFrom, timeTo]; + const timeRange = [timeFrom, timeTo]; - let useTrends = this.isUseTrends(timeRange); + const useTrends = this.isUseTrends(timeRange); - // Metrics or Text query mode - if (!target.mode || target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT) { + // Metrics or Text query + if (!target.queryType || target.queryType === c.MODE_METRICS || target.queryType === c.MODE_TEXT) { // Don't request undefined targets if (!target.group || !target.host || !target.item) { return []; } - if (!target.mode || target.mode === c.MODE_METRICS) { + if (!target.queryType || target.queryType === c.MODE_METRICS) { return this.queryNumericData(target, timeRange, useTrends, options); - } else if (target.mode === c.MODE_TEXT) { + } else if (target.queryType === c.MODE_TEXT) { return this.queryTextData(target, timeRange); + } else { + return []; } - } else if (target.mode === c.MODE_ITEMID) { - // Item ID mode + } else if (target.queryType === c.MODE_ITEMID) { + // Item ID query if (!target.itemids) { return []; } return this.queryItemIdData(target, timeRange, useTrends, options); - } else if (target.mode === c.MODE_ITSERVICE) { - // IT services mode + } else if (target.queryType === c.MODE_ITSERVICE) { + // IT services query return this.queryITServiceData(target, timeRange, options); - } else if (target.mode === c.MODE_TRIGGERS) { - // Triggers mode + } else if (target.queryType === c.MODE_TRIGGERS) { + // Triggers query return this.queryTriggersData(target, timeRange); + } else if (target.queryType === c.MODE_PROBLEMS) { + // Problems query + return this.queryProblems(target, timeRange, options); } else { return []; } @@ -192,7 +207,7 @@ export class ZabbixDatasource extends DataSourceApi { } doTsdbRequest(options) { - const tsdbRequestData = { + const tsdbRequestData: any = { queries: options.targets.map(target => { target.datasourceId = this.datasourceId; target.queryType = 'zabbixAPI'; @@ -205,7 +220,7 @@ export class ZabbixDatasource extends DataSourceApi { tsdbRequestData.to = options.range.to.valueOf().toString(); } - return this.backendSrv.post('/api/tsdb/query', tsdbRequestData); + return getBackendSrv().post('/api/tsdb/query', tsdbRequestData); } /** @@ -224,15 +239,15 @@ export class ZabbixDatasource extends DataSourceApi { ] }; - return this.backendSrv.post('/api/tsdb/query', tsdbRequestData); + return getBackendSrv().post('/api/tsdb/query', tsdbRequestData); } /** - * Query target data for Metrics mode + * Query target data for Metrics */ queryNumericData(target, timeRange, useTrends, options) { let queryStart, queryEnd; - let getItemOptions = { + const getItemOptions = { itemtype: 'num' }; return this.zabbix.getItemsFromTarget(target, getItemOptions) @@ -242,7 +257,7 @@ export class ZabbixDatasource extends DataSourceApi { }).then(result => { queryEnd = new Date().getTime(); if (this.enableDebugLog) { - console.debug(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`); + console.log(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`); } return result; }); @@ -269,18 +284,18 @@ export class ZabbixDatasource extends DataSourceApi { getTrendValueType(target) { // Find trendValue() function and get specified trend value - var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); - var trendValueFunc = _.find(target.functions, func => { + const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); + const trendValueFunc = _.find(target.functions, func => { return _.includes(trendFunctions, func.def.name); }); return trendValueFunc ? trendValueFunc.params[0] : "avg"; } applyDataProcessingFunctions(timeseries_data, target) { - let transformFunctions = bindFunctionDefs(target.functions, 'Transform'); - let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); - let filterFunctions = bindFunctionDefs(target.functions, 'Filter'); - let aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); + const transformFunctions = bindFunctionDefs(target.functions, 'Transform'); + const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); + const filterFunctions = bindFunctionDefs(target.functions, 'Filter'); + const aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); // Apply transformation functions timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => { @@ -298,8 +313,8 @@ export class ZabbixDatasource extends DataSourceApi { let dp = _.map(timeseries_data, 'datapoints'); dp = utils.sequence(aggregationFunctions)(dp); - let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); - let lastAgg = _.findLast(target.functions, func => { + const aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); + const lastAgg = _.findLast(target.functions, func => { return _.includes(aggFuncNames, func.def.name); }); @@ -310,7 +325,7 @@ export class ZabbixDatasource extends DataSourceApi { } // Apply alias functions - _.forEach(timeseries_data, utils.sequence(aliasFunctions)); + _.forEach(timeseries_data, utils.sequence(aliasFunctions).bind(this)); // Apply Time-related functions (timeShift(), etc) // Find timeShift() function and get specified trend value @@ -321,11 +336,11 @@ export class ZabbixDatasource extends DataSourceApi { applyTimeShiftFunction(timeseries_data, target) { // Find timeShift() function and get specified interval - let timeShiftFunc = _.find(target.functions, (func) => { + const timeShiftFunc = _.find(target.functions, (func) => { return func.def.name === 'timeShift'; }); if (timeShiftFunc) { - let shift = timeShiftFunc.params[0]; + const shift = timeShiftFunc.params[0]; _.forEach(timeseries_data, (series) => { series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints); }); @@ -333,10 +348,10 @@ export class ZabbixDatasource extends DataSourceApi { } /** - * Query target data for Text mode + * Query target data for Text */ queryTextData(target, timeRange) { - let options = { + const options = { itemtype: 'text' }; return this.zabbix.getItemsFromTarget(target, options) @@ -346,7 +361,7 @@ export class ZabbixDatasource extends DataSourceApi { } /** - * Query target data for Item ID mode + * Query target data for Item ID */ queryItemIdData(target, timeRange, useTrends, options) { let itemids = target.itemids; @@ -364,7 +379,7 @@ export class ZabbixDatasource extends DataSourceApi { } /** - * Query target data for IT Services mode + * Query target data for IT Services */ queryITServiceData(target, timeRange, options) { // Don't show undefined and hidden targets @@ -382,21 +397,26 @@ export class ZabbixDatasource extends DataSourceApi { itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars); } + options.slaInterval = target.slaInterval; + return this.zabbix.getITServices(itServiceFilter) .then(itservices => { + if (options.isOldVersion) { + itservices = _.filter(itservices, {'serviceid': target.itservice?.serviceid}); + } return this.zabbix.getSLA(itservices, timeRange, target, options);}) .then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target)); } queryTriggersData(target, timeRange) { - let [timeFrom, timeTo] = timeRange; + const [timeFrom, timeTo] = timeRange; return this.zabbix.getHostsFromTarget(target) .then(results => { - let [hosts, apps] = results; + const [hosts, apps] = results; if (hosts.length) { - let hostids = _.map(hosts, 'hostid'); - let appids = _.map(apps, 'applicationid'); - let options = { + const hostids = _.map(hosts, 'hostid'); + const appids = _.map(apps, 'applicationid'); + const options = { minSeverity: target.triggers.minSeverity, acknowledged: target.triggers.acknowledged, count: target.triggers.count, @@ -417,6 +437,78 @@ export class ZabbixDatasource extends DataSourceApi { }); } + queryProblems(target, timeRange, options) { + const [timeFrom, timeTo] = timeRange; + const userIsEditor = contextSrv.isEditor || contextSrv.isGrafanaAdmin; + + let proxies; + let showAckButton = true; + + const showProblems = target.showProblems || ShowProblemTypes.Problems; + const showProxy = target.options.hostProxy; + + const getProxiesPromise = showProxy ? this.zabbix.getProxies() : () => []; + showAckButton = !this.disableReadOnlyUsersAck || userIsEditor; + + // Replace template variables + const groupFilter = this.replaceTemplateVars(target.group?.filter, options.scopedVars); + const hostFilter = this.replaceTemplateVars(target.host?.filter, options.scopedVars); + const appFilter = this.replaceTemplateVars(target.application?.filter, options.scopedVars); + const proxyFilter = this.replaceTemplateVars(target.proxy?.filter, options.scopedVars); + + const triggerFilter = this.replaceTemplateVars(target.trigger?.filter, options.scopedVars); + const tagsFilter = this.replaceTemplateVars(target.tags?.filter, options.scopedVars); + + const replacedTarget = { + ...target, + trigger: { filter: triggerFilter }, + tags: { filter: tagsFilter }, + }; + + const problemsOptions: any = { + recent: showProblems === ShowProblemTypes.Recent, + minSeverity: target.options?.minSeverity, + limit: target.options?.limit, + }; + + if (target.options?.acknowledged === 0 || target.options?.acknowledged === 1) { + problemsOptions.acknowledged = target.options?.acknowledged ? true : false; + } + + if (target.options?.minSeverity) { + const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= target.options?.minSeverity); + problemsOptions.severities = severities; + } + + if (showProblems === ShowProblemTypes.History) { + problemsOptions.timeFrom = timeFrom; + problemsOptions.timeTo = timeTo; + } + + const getProblemsPromise = showProblems === ShowProblemTypes.History ? + this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions) : + this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions); + + const problemsPromises = Promise.all([ + getProblemsPromise, + getProxiesPromise + ]) + .then(([problems, sourceProxies]) => { + proxies = _.keyBy(sourceProxies, 'proxyid'); + return problems; + }) + .then(problems => problemsHandler.setMaintenanceStatus(problems)) + .then(problems => problemsHandler.setAckButtonStatus(problems, showAckButton)) + .then(problems => problemsHandler.filterTriggersPre(problems, replacedTarget)) + .then(problems => problemsHandler.addTriggerDataSource(problems, target)) + .then(problems => problemsHandler.addTriggerHostProxy(problems, proxies)); + + return problemsPromises.then(problems => { + const problemsDataFrame = problemsHandler.toDataFrame(problems); + return problemsDataFrame; + }); + } + /** * Test connection to Zabbix API and external history DB. */ @@ -436,30 +528,26 @@ export class ZabbixDatasource extends DataSourceApi { title: "Success", message: message }; - } - catch (error) { + } catch (error) { if (error instanceof ZabbixAPIError) { return { status: "error", title: error.message, message: error.message }; - } - else if (error.data && error.data.message) { + } else if (error.data && error.data.message) { return { status: "error", title: "Zabbix Client Error", message: error.data.message }; - } - else if (typeof (error) === 'string') { + } else if (typeof (error) === 'string') { return { status: "error", title: "Unknown Error", message: error }; - } - else { + } else { console.log(error); return { status: "error", @@ -470,20 +558,6 @@ export class ZabbixDatasource extends DataSourceApi { } } - /** - * Get Zabbix version - */ - getVersion() { - return this.zabbix.getVersion() - .then(version => { - const zabbixVersion = utils.parseVersion(version); - if (!zabbixVersion) { - return null; - } - return zabbixVersion.major; - }); - } - //////////////// // Templating // //////////////// @@ -495,7 +569,7 @@ export class ZabbixDatasource extends DataSourceApi { * @return {string} Metric name - group, host, app or item or list * of metrics in "{metric1,metcic2,...,metricN}" format. */ - metricFindQuery(query) { + metricFindQuery(query, options) { let resultPromise; let queryModel = _.cloneDeep(query); @@ -512,6 +586,8 @@ export class ZabbixDatasource extends DataSourceApi { queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {}); } + const { group, host, application, item } = queryModel; + switch (queryModel.queryType) { case VariableQueryTypes.Group: resultPromise = this.zabbix.getGroups(queryModel.group); @@ -525,6 +601,10 @@ export class ZabbixDatasource extends DataSourceApi { case VariableQueryTypes.Item: resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item); break; + case VariableQueryTypes.ItemValues: + const range = options?.range; + resultPromise = this.zabbix.getItemValues(group, host, application, item, { range }); + break; default: resultPromise = Promise.resolve([]); break; @@ -543,71 +623,63 @@ export class ZabbixDatasource extends DataSourceApi { const timeRange = options.range || options.rangeRaw; const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000); const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000); - var annotation = options.annotation; - var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS; + const annotation = options.annotation; // Show all triggers - let triggersOptions = { - showTriggers: c.SHOW_ALL_TRIGGERS, - hideHostsInMaintenance: false + const problemsOptions: any = { + value: annotation.showOkEvents ? ['0', '1'] : '1', + valueFromEvent: true, + timeFrom, + timeTo, }; - var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}), - this.replaceTemplateVars(annotation.host, {}), - this.replaceTemplateVars(annotation.application, {}), - triggersOptions); + if (annotation.minseverity) { + const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity)); + problemsOptions.severities = severities; + } - return getTriggers.then(triggers => { + const groupFilter = this.replaceTemplateVars(annotation.group, {}); + const hostFilter = this.replaceTemplateVars(annotation.host, {}); + const appFilter = this.replaceTemplateVars(annotation.application, {}); + const proxyFilter = undefined; + return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions) + .then(problems => { // Filter triggers by description - let triggerName = this.replaceTemplateVars(annotation.trigger, {}); - if (utils.isRegex(triggerName)) { - triggers = _.filter(triggers, trigger => { - return utils.buildRegex(triggerName).test(trigger.description); + const problemName = this.replaceTemplateVars(annotation.trigger, {}); + if (utils.isRegex(problemName)) { + problems = _.filter(problems, p => { + return utils.buildRegex(problemName).test(p.description); }); - } else if (triggerName) { - triggers = _.filter(triggers, trigger => { - return trigger.description === triggerName; + } else if (problemName) { + problems = _.filter(problems, p => { + return p.description === problemName; }); } - // Remove events below the chose severity - triggers = _.filter(triggers, trigger => { - return Number(trigger.priority) >= Number(annotation.minseverity); - }); - - var objectids = _.map(triggers, 'triggerid'); - return this.zabbix - .getEvents(objectids, timeFrom, timeTo, showOkEvents) - .then(events => { - var indexedTriggers = _.keyBy(triggers, 'triggerid'); - - // Hide acknowledged events if option enabled - if (annotation.hideAcknowledged) { - events = _.filter(events, event => { - return !event.acknowledges.length; - }); - } - - return _.map(events, event => { - let tags; - if (annotation.showHostname) { - tags = _.map(event.hosts, 'name'); - } - - // Show event type (OK or Problem) - let title = Number(event.value) ? 'Problem' : 'OK'; - - let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges); - return { - annotation: annotation, - time: event.clock * 1000, - title: title, - tags: tags, - text: indexedTriggers[event.objectid].description + formatted_acknowledges - }; - }); + // Hide acknowledged events if option enabled + if (annotation.hideAcknowledged) { + problems = _.filter(problems, p => { + return !p.acknowledges?.length; }); + } + + return _.map(problems, p => { + const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges); + + let annotationTags: string[] = []; + if (annotation.showHostname) { + annotationTags = _.map(p.hosts, 'name'); + } + + return { + title: p.value === '1' ? 'Problem' : 'OK', + time: p.timestamp * 1000, + annotation: annotation, + text: p.name + formattedAcknowledges, + tags: annotationTags, + }; + }); }); } @@ -617,8 +689,8 @@ export class ZabbixDatasource extends DataSourceApi { * or empty object if no related triggers are finded. */ alertQuery(options) { - let enabled_targets = filterEnabledTargets(options.targets); - let getPanelItems = _.map(enabled_targets, t => { + const enabled_targets = filterEnabledTargets(options.targets); + const getPanelItems = _.map(enabled_targets, t => { let target = _.cloneDeep(t); target = migrations.migrate(target); this.replaceTargetVariables(target, options); @@ -627,8 +699,8 @@ export class ZabbixDatasource extends DataSourceApi { return Promise.all(getPanelItems) .then(results => { - let items = _.flatten(results); - let itemids = _.map(items, 'itemid'); + const items = _.flatten(results); + const itemids = _.map(items, 'itemid'); if (itemids.length === 0) { return []; @@ -646,12 +718,12 @@ export class ZabbixDatasource extends DataSourceApi { let state = 'ok'; - let firedTriggers = _.filter(triggers, {value: '1'}); + const firedTriggers = _.filter(triggers, {value: '1'}); if (firedTriggers.length) { state = 'alerting'; } - let thresholds = _.map(triggers, trigger => { + const thresholds = _.map(triggers, trigger => { return getTriggerThreshold(trigger.expression); }); @@ -665,7 +737,7 @@ export class ZabbixDatasource extends DataSourceApi { // Replace template variables replaceTargetVariables(target, options) { - let parts = ['group', 'host', 'application', 'item']; + const parts = ['group', 'host', 'application', 'item']; _.forEach(parts, p => { if (target[p] && target[p].filter) { target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars); @@ -685,10 +757,10 @@ export class ZabbixDatasource extends DataSourceApi { } isUseTrends(timeRange) { - let [timeFrom, timeTo] = timeRange; - let useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); - let useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000); - let useTrends = this.trends && ( + const [timeFrom, timeTo] = timeRange; + const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); + const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000); + const useTrends = this.trends && ( (timeFrom < useTrendsFrom) || (timeTo - timeFrom > useTrendsRange) ); @@ -697,20 +769,20 @@ export class ZabbixDatasource extends DataSourceApi { } function bindFunctionDefs(functionDefs, category) { - var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); - var aggFuncDefs = _.filter(functionDefs, function(func) { + const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); + const aggFuncDefs = _.filter(functionDefs, func => { return _.includes(aggregationFunctions, func.def.name); }); - return _.map(aggFuncDefs, function(func) { - var funcInstance = metricFunctions.createFuncInstance(func.def, func.params); + return _.map(aggFuncDefs, func => { + const funcInstance = metricFunctions.createFuncInstance(func.def, func.params); return funcInstance.bindFunction(dataProcessor.metricFunctions); }); } function getConsolidateBy(target) { let consolidateBy; - let funcDef = _.find(target.functions, func => { + const funcDef = _.find(target.functions, func => { return func.def.name === 'consolidateBy'; }); if (funcDef && funcDef.params && funcDef.params.length) { @@ -720,8 +792,8 @@ function getConsolidateBy(target) { } function downsampleSeries(timeseries_data, options) { - let defaultAgg = dataProcessor.aggregationFunctions['avg']; - let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg; + const defaultAgg = dataProcessor.aggregationFunctions['avg']; + const consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg; return _.map(timeseries_data, timeseries => { if (timeseries.datapoints.length > options.maxDataPoints) { timeseries.datapoints = dataProcessor @@ -753,7 +825,7 @@ export function zabbixTemplateFormat(value) { return utils.escapeRegex(value); } - var escapedValues = _.map(value, utils.escapeRegex); + const escapedValues = _.map(value, utils.escapeRegex); return '(' + escapedValues.join('|') + ')'; } @@ -773,7 +845,7 @@ function zabbixItemIdsTemplateFormat(value) { * /$variable/ -> /a|b|c/ -> /a|b|c/ */ function replaceTemplateVars(templateSrv, target, scopedVars) { - var replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat); + let replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat); if (target !== replacedTarget && !utils.isRegex(replacedTarget)) { replacedTarget = '/^' + replacedTarget + '$/'; } @@ -787,8 +859,8 @@ function filterEnabledTargets(targets) { } function getTriggerThreshold(expression) { - let thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/; - let finded_thresholds = expression.match(thresholdPattern); + const thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/; + const finded_thresholds = expression.match(thresholdPattern); if (finded_thresholds && finded_thresholds.length >= 2) { let threshold = finded_thresholds[1]; threshold = Number(threshold); @@ -797,7 +869,3 @@ function getTriggerThreshold(expression) { return null; } } - -// Fix for backward compatibility with lodash 2.4 -if (!_.includes) {_.includes = _.contains;} -if (!_.keyBy) {_.keyBy = _.indexBy;} diff --git a/src/datasource-zabbix/img/icn-zabbix-datasource.svg b/src/datasource-zabbix/img/icn-zabbix-datasource.svg new file mode 100644 index 0000000..6878e22 --- /dev/null +++ b/src/datasource-zabbix/img/icn-zabbix-datasource.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/datasource-zabbix/img/zabbix_app_logo.svg b/src/datasource-zabbix/img/zabbix_app_logo.svg deleted file mode 100755 index 237247d..0000000 --- a/src/datasource-zabbix/img/zabbix_app_logo.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/src/datasource-zabbix/metricFunctions.js b/src/datasource-zabbix/metricFunctions.ts similarity index 92% rename from src/datasource-zabbix/metricFunctions.js rename to src/datasource-zabbix/metricFunctions.ts index 150e03a..fdad893 100644 --- a/src/datasource-zabbix/metricFunctions.js +++ b/src/datasource-zabbix/metricFunctions.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; -import $ from 'jquery'; +import { isNumeric } from './utils'; -var index = []; -var categories = { +const index = []; +const categories = { Transform: [], Aggregate: [], Filter: [], @@ -298,11 +298,15 @@ addFuncDef({ defaultParams: ['avg'], }); -_.each(categories, function(funcList, catName) { +_.each(categories, (funcList, catName) => { categories[catName] = _.sortBy(funcList, 'name'); }); class FuncInstance { + def: any; + params: any; + text: string; + constructor(funcDef, params) { this.def = funcDef; @@ -318,13 +322,13 @@ class FuncInstance { } bindFunction(metricFunctions) { - var func = metricFunctions[this.def.name]; + const func = metricFunctions[this.def.name]; if (func) { // Bind function arguments - var bindedFunc = func; - var param; - for (var i = 0; i < this.params.length; i++) { + let bindedFunc = func; + let param; + for (let i = 0; i < this.params.length; i++) { param = this.params[i]; // Convert numeric params @@ -341,23 +345,21 @@ class FuncInstance { } render(metricExp) { - var str = this.def.name + '('; - var parameters = _.map(this.params, function(value, index) { - - var paramType = this.def.params[index].type; + const str = this.def.name + '('; + const parameters = _.map(this.params, (value, index) => { + const paramType = this.def.params[index].type; if (paramType === 'int' || paramType === 'float' || paramType === 'value_or_series' || paramType === 'boolean') { return value; - } - else if (paramType === 'int_or_interval' && $.isNumeric(value)) { + } else if (paramType === 'int_or_interval' && isNumeric(value)) { return value; } return "'" + value + "'"; - }, this); + }); if (metricExp) { parameters.unshift(metricExp); @@ -378,16 +380,15 @@ class FuncInstance { // handle optional parameters // if string contains ',' and next param is optional, split and update both if (this._hasMultipleParamsInString(strValue, index)) { - _.each(strValue.split(','), function(partVal, idx) { + _.each(strValue.split(','), (partVal, idx) => { this.updateParam(partVal.trim(), idx); - }, this); + }); return; } if (strValue === '' && this.def.params[index].optional) { this.params.splice(index, 1); - } - else { + }else { this.params[index] = strValue; } @@ -400,7 +401,7 @@ class FuncInstance { return; } - var text = this.def.name + '('; + let text = this.def.name + '('; text += this.params.join(', '); text += ')'; this.text = text; diff --git a/src/datasource-zabbix/migrations.ts b/src/datasource-zabbix/migrations.ts index 01417d3..f786ec9 100644 --- a/src/datasource-zabbix/migrations.ts +++ b/src/datasource-zabbix/migrations.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import { ZabbixMetricsQuery } from './types'; +import * as c from './constants'; /** * Query format migration. @@ -28,6 +29,34 @@ export function migrateFrom2To3version(target: ZabbixMetricsQuery) { return target; } +function migratePercentileAgg(target) { + if (target.functions) { + for (const f of target.functions) { + if (f.def && f.def.name === 'percentil') { + f.def.name = 'percentile'; + } + } + } +} + +function migrateQueryType(target) { + if (target.queryType === undefined) { + if (target.mode === 'Metrics') { + // Explore mode + target.queryType = c.MODE_METRICS; + } else if (target.mode !== undefined) { + target.queryType = target.mode; + delete target.mode; + } + } +} + +function migrateSLA(target) { + if (target.queryType === c.MODE_ITSERVICE && !target.slaInterval) { + target.slaInterval = 'none'; + } +} + export function migrate(target) { target.resultFormat = target.resultFormat || 'time_series'; target = fixTargetGroup(target); @@ -35,6 +64,8 @@ export function migrate(target) { return migrateFrom2To3version(target); } migratePercentileAgg(target); + migrateQueryType(target); + migrateSLA(target); return target; } @@ -53,16 +84,6 @@ function convertToRegex(str) { } } -function migratePercentileAgg(target) { - if (target.functions) { - for (const f of target.functions) { - if (f.def && f.def.name === 'percentil') { - f.def.name = 'percentile'; - } - } - } -} - export const DS_CONFIG_SCHEMA = 2; export function migrateDSConfig(jsonData) { if (!jsonData) { diff --git a/src/datasource-zabbix/partials/config.html b/src/datasource-zabbix/partials/config.html index 6b389f3..83c5ed9 100644 --- a/src/datasource-zabbix/partials/config.html +++ b/src/datasource-zabbix/partials/config.html @@ -73,15 +73,6 @@ placeholder="1h">
- -
- Zabbix version -
- -
-
diff --git a/src/datasource-zabbix/partials/query.editor.html b/src/datasource-zabbix/partials/query.editor.html index ccf5ee3..0aabb48 100644 --- a/src/datasource-zabbix/partials/query.editor.html +++ b/src/datasource-zabbix/partials/query.editor.html @@ -5,14 +5,14 @@
-
- +
+
@@ -23,7 +23,7 @@
-
+
- +
+ +
+
-
+
@@ -71,8 +81,8 @@ }">
-
- +
+
+
+ + +
+
-
+
@@ -109,8 +134,8 @@
-
- +
+
-
- -
- -
-
-
- -
- -
+
+ +
- - +
+ + +
- -
-
- - +
+
+ +
+ +
-
- - +
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + + +
+
-
+
-
+
@@ -215,23 +271,82 @@
- -
- -
- - + + + +
+
+ + +
+
+ +
- - -
-
+
+ + +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ + + + +
+ + +
+
diff --git a/src/datasource-zabbix/plugin.json b/src/datasource-zabbix/plugin.json index f0cd0d2..356af3f 100644 --- a/src/datasource-zabbix/plugin.json +++ b/src/datasource-zabbix/plugin.json @@ -38,8 +38,8 @@ "url": "https://github.com/alexanderzobnin/grafana-zabbix" }, "logos": { - "small": "img/zabbix_app_logo.svg", - "large": "img/zabbix_app_logo.svg" + "small": "img/icn-zabbix-datasource.svg", + "large": "img/icn-zabbix-datasource.svg" } } } diff --git a/src/datasource-zabbix/problemsHandler.ts b/src/datasource-zabbix/problemsHandler.ts new file mode 100644 index 0000000..197b583 --- /dev/null +++ b/src/datasource-zabbix/problemsHandler.ts @@ -0,0 +1,201 @@ +import _ from 'lodash'; +import * as utils from '../datasource-zabbix/utils'; +import { DataFrame, Field, FieldType, ArrayVector } from '@grafana/data'; +import { ZBXProblem, ZBXTrigger, ProblemDTO, ZBXEvent } from './types'; + +export function joinTriggersWithProblems(problems: ZBXProblem[], triggers: ZBXTrigger[]): ProblemDTO[] { + const problemDTOList: ProblemDTO[] = []; + + for (let i = 0; i < problems.length; i++) { + const p = problems[i]; + const triggerId = Number(p.objectid); + const t = triggers[triggerId]; + + if (t) { + const problemDTO: ProblemDTO = { + timestamp: Number(p.clock), + triggerid: p.objectid, + eventid: p.eventid, + name: p.name, + severity: p.severity, + acknowledged: p.acknowledged, + acknowledges: p.acknowledges, + tags: p.tags, + suppressed: p.suppressed, + suppression_data: p.suppression_data, + description: t.description, + comments: t.comments, + value: t.value, + groups: t.groups, + hosts: t.hosts, + items: t.items, + alerts: t.alerts, + url: t.url, + expression: t.expression, + correlation_mode: t.correlation_mode, + correlation_tag: t.correlation_tag, + manual_close: t.manual_close, + state: t.state, + error: t.error, + }; + + problemDTOList.push(problemDTO); + } + + } + + return problemDTOList; +} + +interface JoinOptions { + valueFromEvent?: boolean; +} + +export function joinTriggersWithEvents(events: ZBXEvent[], triggers: ZBXTrigger[], options?: JoinOptions): ProblemDTO[] { + const { valueFromEvent } = options; + const problemDTOList: ProblemDTO[] = []; + + for (let i = 0; i < events.length; i++) { + const e = events[i]; + const triggerId = Number(e.objectid); + const t = triggers[triggerId]; + + if (t) { + const problemDTO: ProblemDTO = { + value: valueFromEvent ? e.value : t.value, + timestamp: Number(e.clock), + triggerid: e.objectid, + eventid: e.eventid, + name: e.name, + severity: e.severity, + acknowledged: e.acknowledged, + acknowledges: e.acknowledges, + tags: e.tags, + suppressed: e.suppressed, + description: t.description, + comments: t.comments, + groups: t.groups, + hosts: t.hosts, + items: t.items, + alerts: t.alerts, + url: t.url, + expression: t.expression, + correlation_mode: t.correlation_mode, + correlation_tag: t.correlation_tag, + manual_close: t.manual_close, + state: t.state, + error: t.error, + }; + + problemDTOList.push(problemDTO); + } + + } + + return problemDTOList; +} + +export function setMaintenanceStatus(triggers) { + _.each(triggers, (trigger) => { + const maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1'); + trigger.maintenance = maintenance_status; + }); + return triggers; +} + +export function setAckButtonStatus(triggers, showAckButton) { + _.each(triggers, (trigger) => { + trigger.showAckButton = showAckButton; + }); + return triggers; +} + +export function addTriggerDataSource(triggers, target) { + _.each(triggers, (trigger) => { + trigger.datasource = target.datasource; + }); + return triggers; +} + +export function addTriggerHostProxy(triggers, proxies) { + triggers.forEach(trigger => { + if (trigger.hosts && trigger.hosts.length) { + const host = trigger.hosts[0]; + if (host.proxy_hostid !== '0') { + const hostProxy = proxies[host.proxy_hostid]; + host.proxy = hostProxy ? hostProxy.host : ''; + } + } + }); + return triggers; +} + +export function filterTriggersPre(triggerList, replacedTarget) { + // Filter triggers by description + const triggerFilter = replacedTarget.trigger.filter; + if (triggerFilter) { + triggerList = filterTriggers(triggerList, triggerFilter); + } + + // Filter by tags + if (replacedTarget.tags.filter) { + let tagsFilter = replacedTarget.tags.filter; + // replaceTemplateVars() builds regex-like string, so we should trim it. + tagsFilter = tagsFilter.replace('/^', '').replace('$/', ''); + const tags = utils.parseTags(tagsFilter); + triggerList = _.filter(triggerList, trigger => { + return _.every(tags, tag => { + return _.find(trigger.tags, t => t.tag === tag.tag && (!tag.value || t.value === tag.value)); + }); + }); + } + + // Filter by maintenance status + if (!replacedTarget.options.hostsInMaintenance) { + triggerList = _.filter(triggerList, (trigger) => !trigger.maintenance); + } + + return triggerList; +} + +function filterTriggers(triggers, triggerFilter) { + if (utils.isRegex(triggerFilter)) { + return _.filter(triggers, trigger => { + return utils.buildRegex(triggerFilter).test(trigger.description); + }); + } else { + return _.filter(triggers, trigger => { + return trigger.description === triggerFilter; + }); + } +} + +export function toDataFrame(problems: any[]): DataFrame { + const problemsField: Field = { + name: 'Problems', + type: FieldType.other, + values: new ArrayVector(problems), + config: {}, + }; + + const response: DataFrame = { + name: 'problems', + fields: [problemsField], + length: problems.length, + }; + + return response; +} + +const problemsHandler = { + addTriggerDataSource, + addTriggerHostProxy, + setMaintenanceStatus, + setAckButtonStatus, + filterTriggersPre, + toDataFrame, + joinTriggersWithProblems, + joinTriggersWithEvents, +}; + +export default problemsHandler; diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js index eb1b6a2..2515869 100644 --- a/src/datasource-zabbix/query.controller.js +++ b/src/datasource-zabbix/query.controller.js @@ -4,6 +4,58 @@ import * as c from './constants'; import * as utils from './utils'; import * as metricFunctions from './metricFunctions'; import * as migrations from './migrations'; +import { ShowProblemTypes } from './types'; + +function getTargetDefaults() { + return { + queryType: c.MODE_METRICS, + group: { 'filter': "" }, + host: { 'filter': "" }, + application: { 'filter': "" }, + item: { 'filter': "" }, + functions: [], + triggers: { + 'count': true, + 'minSeverity': 3, + 'acknowledged': 2 + }, + trigger: {filter: ""}, + tags: {filter: ""}, + proxy: {filter: ""}, + options: { + showDisabledItems: false, + skipEmptyValues: false, + }, + table: { + 'skipEmptyValues': false + }, + }; +} + +function getSLATargetDefaults() { + return { + slaProperty: { name: "SLA", property: "sla" }, + slaInterval: 'none', + }; +} + +function getProblemsTargetDefaults() { + return { + showProblems: ShowProblemTypes.Problems, + options: { + minSeverity: 0, + sortProblems: 'default', + acknowledged: 2, + hostsInMaintenance: false, + hostProxy: false, + limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT, + }, + }; +} + +function getSeverityOptions() { + return c.TRIGGER_SEVERITY; +} export class ZabbixQueryController extends QueryCtrl { @@ -17,11 +69,12 @@ export class ZabbixQueryController extends QueryCtrl { this.templateSrv = templateSrv; this.editorModes = [ - {value: 'num', text: 'Metrics', mode: c.MODE_METRICS}, - {value: 'text', text: 'Text', mode: c.MODE_TEXT}, - {value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE}, - {value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID}, - {value: 'triggers', text: 'Triggers', mode: c.MODE_TRIGGERS} + {value: 'num', text: 'Metrics', queryType: c.MODE_METRICS}, + {value: 'text', text: 'Text', queryType: c.MODE_TEXT}, + {value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE}, + {value: 'itemid', text: 'Item ID', queryType: c.MODE_ITEMID}, + {value: 'triggers', text: 'Triggers', queryType: c.MODE_TRIGGERS}, + {value: 'problems', text: 'Problems', queryType: c.MODE_PROBLEMS}, ]; this.$scope.editorMode = { @@ -29,7 +82,8 @@ export class ZabbixQueryController extends QueryCtrl { TEXT: c.MODE_TEXT, ITSERVICE: c.MODE_ITSERVICE, ITEMID: c.MODE_ITEMID, - TRIGGERS: c.MODE_TRIGGERS + TRIGGERS: c.MODE_TRIGGERS, + PROBLEMS: c.MODE_PROBLEMS, }; this.slaPropertyList = [ @@ -40,15 +94,49 @@ export class ZabbixQueryController extends QueryCtrl { {name: "Down time", property: "downtimeTime"} ]; + this.slaIntervals = [ + { text: 'No interval', value: 'none' }, + { text: 'Auto', value: 'auto' }, + { text: '1 hour', value: '1h' }, + { text: '12 hours', value: '12h' }, + { text: '24 hours', value: '1d' }, + { text: '1 week', value: '1w' }, + { text: '1 month', value: '1M' }, + ]; + this.ackFilters = [ {text: 'all triggers', value: 2}, {text: 'unacknowledged', value: 0}, {text: 'acknowledged', value: 1}, ]; + this.problemAckFilters = [ + 'all triggers', + 'unacknowledged', + 'acknowledged' + ]; + + this.sortByFields = [ + { text: 'Default', value: 'default' }, + { text: 'Last change', value: 'lastchange' }, + { text: 'Severity', value: 'priority' }, + ]; + + this.showEventsFields = [ + { text: 'All', value: [0,1] }, + { text: 'OK', value: [0] }, + { text: 'Problems', value: 1 } + ]; + + this.showProblemsOptions = [ + { text: 'Problems', value: 'problems' }, + { text: 'Recent problems', value: 'recent' }, + { text: 'History', value: 'history' }, + ]; + this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; - this.triggerSeverity = c.TRIGGER_SEVERITY; + this.severityOptions = getSeverityOptions(); // Map functions for bs-typeahead this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList'); @@ -56,6 +144,7 @@ export class ZabbixQueryController extends QueryCtrl { this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList'); this.getItemNames = _.bind(this.getMetricNames, this, 'itemList'); this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList'); + this.getProxyNames = _.bind(this.getMetricNames, this, 'proxyList'); this.getVariables = _.bind(this.getTemplateVariables, this); // Update metric suggestion when template variable was changed @@ -80,40 +169,32 @@ export class ZabbixQueryController extends QueryCtrl { _.defaults(this, scopeDefaults); // Load default values - var targetDefaults = { - 'mode': c.MODE_METRICS, - 'group': { 'filter': "" }, - 'host': { 'filter': "" }, - 'application': { 'filter': "" }, - 'item': { 'filter': "" }, - 'functions': [], - 'triggers': { - 'count': true, - 'minSeverity': 3, - 'acknowledged': 2 - }, - 'options': { - 'showDisabledItems': false, - 'skipEmptyValues': false - }, - 'table': { - 'skipEmptyValues': false - } - }; - _.defaults(target, targetDefaults); + const targetDefaults = getTargetDefaults(); + _.defaultsDeep(target, targetDefaults); + + if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) { + target.queryType = c.MODE_PROBLEMS; + } // Create function instances from saved JSON target.functions = _.map(target.functions, function(func) { return metricFunctions.createFuncInstance(func.def, func.params); }); - if (target.mode === c.MODE_METRICS || - target.mode === c.MODE_TEXT || - target.mode === c.MODE_TRIGGERS) { - this.initFilters(); + if (target.queryType === c.MODE_ITSERVICE) { + _.defaultsDeep(target, getSLATargetDefaults()); } - else if (target.mode === c.MODE_ITSERVICE) { - _.defaults(target, {slaProperty: {name: "SLA", property: "sla"}}); + + if (target.queryType === c.MODE_PROBLEMS) { + _.defaultsDeep(target, getProblemsTargetDefaults()); + } + + if (target.queryType === c.MODE_METRICS || + target.queryType === c.MODE_TEXT || + target.queryType === c.MODE_TRIGGERS || + target.queryType === c.MODE_PROBLEMS) { + this.initFilters(); + } else if (target.queryType === c.MODE_ITSERVICE) { this.suggestITServices(); } }; @@ -123,14 +204,20 @@ export class ZabbixQueryController extends QueryCtrl { } initFilters() { - let itemtype = _.find(this.editorModes, {'mode': this.target.mode}); + let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType}); itemtype = itemtype ? itemtype.value : null; - return Promise.all([ + const promises = [ this.suggestGroups(), this.suggestHosts(), this.suggestApps(), - this.suggestItems(itemtype) - ]); + this.suggestItems(itemtype), + ]; + + if (this.target.queryType === c.MODE_PROBLEMS) { + promises.push(this.suggestProxies()); + } + + return Promise.all(promises); } // Get list of metric names for bs-typeahead directive @@ -207,6 +294,15 @@ export class ZabbixQueryController extends QueryCtrl { }); } + suggestProxies() { + return this.zabbix.getProxies() + .then(response => { + const proxies = _.map(response, 'host'); + this.metric.proxyList = proxies; + return proxies; + }); + } + isRegex(str) { return utils.isRegex(str); } @@ -302,19 +398,42 @@ export class ZabbixQueryController extends QueryCtrl { } renderQueryOptionsText() { - var optionsMap = { + const metricOptionsMap = { showDisabledItems: "Show disabled items", - skipEmptyValues: "Skip empty values" }; - var options = []; + + const problemsOptionsMap = { + sortProblems: "Sort problems", + acknowledged: "Acknowledged", + skipEmptyValues: "Skip empty values", + hostsInMaintenance: "Show hosts in maintenance", + limit: "Limit problems", + hostProxy: "Show proxy", + }; + + let optionsMap = {}; + + if (this.target.queryType === c.MODE_METRICS) { + optionsMap = metricOptionsMap; + } else if (this.target.queryType === c.MODE_PROBLEMS || this.target.queryType === c.MODE_TRIGGERS) { + optionsMap = problemsOptionsMap; + } + + const options = []; _.forOwn(this.target.options, (value, key) => { - if (value) { + if (value && optionsMap[key]) { if (value === true) { // Show only option name (if enabled) for boolean options options.push(optionsMap[key]); } else { // Show "option = value" for another options - options.push(optionsMap[key] + " = " + value); + let optionValue = value; + if (value && value.text) { + optionValue = value.text; + } else if (value && value.value) { + optionValue = value.value; + } + options.push(optionsMap[key] + " = " + optionValue); } } }); @@ -329,7 +448,8 @@ export class ZabbixQueryController extends QueryCtrl { * 2 - Text metrics */ switchEditorMode(mode) { - this.target.mode = mode; + this.target.queryType = mode; + this.queryOptionsText = this.renderQueryOptionsText(); this.init(); this.targetChanged(); } diff --git a/src/datasource-zabbix/query_help.md b/src/datasource-zabbix/query_help.md index bb6c526..0b0fd31 100644 --- a/src/datasource-zabbix/query_help.md +++ b/src/datasource-zabbix/query_help.md @@ -29,3 +29,7 @@ This mode is suitable for rendering charts in grafana by passing itemids as url ##### Triggers Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard. + +#### Documentation links: + +[Grafana-Zabbix Documentation](https://alexanderzobnin.github.io/grafana-zabbix) diff --git a/src/datasource-zabbix/responseHandler.js b/src/datasource-zabbix/responseHandler.ts similarity index 72% rename from src/datasource-zabbix/responseHandler.js rename to src/datasource-zabbix/responseHandler.ts index 85bce2c..cb46c65 100644 --- a/src/datasource-zabbix/responseHandler.js +++ b/src/datasource-zabbix/responseHandler.ts @@ -23,19 +23,35 @@ function convertHistory(history, items, addHostName, convertPointCallback) { */ // Group history by itemid - var grouped_history = _.groupBy(history, 'itemid'); - var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate + const grouped_history = _.groupBy(history, 'itemid'); + const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate - return _.map(grouped_history, function(historyPoint, itemid) { - var item = _.find(items, {'itemid': itemid}); - var alias = item.name; - if (_.keys(hosts).length > 1 && addHostName) { //only when actual multi hosts selected - var host = _.find(hosts, {'hostid': item.hostid}); - alias = host.name + ": " + alias; + return _.map(grouped_history, (hist, itemid) => { + const item = _.find(items, {'itemid': itemid}) as any; + let alias = item.name; + + // Add scopedVars for using in alias functions + const scopedVars: any = { + '__zbx_item': { value: item.name }, + '__zbx_item_name': { value: item.name }, + '__zbx_item_key': { value: item.key_ }, + }; + + if (_.keys(hosts).length > 0) { + const host = _.find(hosts, {'hostid': item.hostid}); + scopedVars['__zbx_host'] = { value: host.host }; + scopedVars['__zbx_host_name'] = { value: host.name }; + + // Only add host when multiple hosts selected + if (_.keys(hosts).length > 1 && addHostName) { + alias = host.name + ": " + alias; + } } + return { target: alias, - datapoints: _.map(historyPoint, convertPointCallback) + datapoints: _.map(hist, convertPointCallback), + scopedVars, }; }); } @@ -53,29 +69,29 @@ function handleHistory(history, items, addHostName = true) { } function handleTrends(history, items, valueType, addHostName = true) { - var convertPointCallback = _.partial(convertTrendPoint, valueType); + const convertPointCallback = _.partial(convertTrendPoint, valueType); return convertHistory(history, items, addHostName, convertPointCallback); } function handleText(history, items, target, addHostName = true) { - let convertTextCallback = _.partial(convertText, target); + const convertTextCallback = _.partial(convertText, target); return convertHistory(history, items, addHostName, convertTextCallback); } function handleHistoryAsTable(history, items, target) { - let table = new TableModel(); + const table: any = new TableModel(); table.addColumn({text: 'Host'}); table.addColumn({text: 'Item'}); table.addColumn({text: 'Key'}); table.addColumn({text: 'Last value'}); - let grouped_history = _.groupBy(history, 'itemid'); + const grouped_history = _.groupBy(history, 'itemid'); _.each(items, (item) => { - let itemHistory = grouped_history[item.itemid] || []; - let lastPoint = _.last(itemHistory); + const itemHistory = grouped_history[item.itemid] || []; + const lastPoint = _.last(itemHistory); let lastValue = lastPoint ? lastPoint.value : null; - if(target.options.skipEmptyValues && (!lastValue || lastValue === '')) { + if (target.options.skipEmptyValues && (!lastValue || lastValue === '')) { return; } @@ -84,7 +100,7 @@ function handleHistoryAsTable(history, items, target) { lastValue = extractText(lastValue, target.textFilter, target.useCaptureGroups); } - let host = _.first(item.hosts); + let host: any = _.first(item.hosts); host = host ? host.name : ""; table.rows.push([ @@ -110,22 +126,22 @@ function convertText(target, point) { } function extractText(str, pattern, useCaptureGroups) { - let extractPattern = new RegExp(pattern); - let extractedValue = extractPattern.exec(str); + const extractPattern = new RegExp(pattern); + const extractedValue = extractPattern.exec(str); if (extractedValue) { if (useCaptureGroups) { - extractedValue = extractedValue[1]; + return extractedValue[1]; } else { - extractedValue = extractedValue[0]; + return extractedValue[0]; } } - return extractedValue; + return ""; } function handleSLAResponse(itservice, slaProperty, slaObject) { - var targetSLA = slaObject[itservice.serviceid].sla; + const targetSLA = slaObject[itservice.serviceid].sla; if (slaProperty.property === 'status') { - var targetStatus = parseInt(slaObject[itservice.serviceid].status); + const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10); return { target: itservice.name + ' ' + slaProperty.name, datapoints: [ @@ -134,7 +150,7 @@ function handleSLAResponse(itservice, slaProperty, slaObject) { }; } else { let i; - let slaArr = []; + const slaArr = []; for (i = 0; i < targetSLA.length; i++) { if (i === 0) { slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]); @@ -165,7 +181,7 @@ function handleTriggersResponse(triggers, groups, timeRange) { } else { const stats = getTriggerStats(triggers); const groupNames = _.map(groups, 'name'); - let table = new TableModel(); + const table: any = new TableModel(); table.addColumn({text: 'Host group'}); _.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => { table.addColumn({text: severity.text}); @@ -182,11 +198,11 @@ function handleTriggersResponse(triggers, groups, timeRange) { } function getTriggerStats(triggers) { - let groups = _.uniq(_.flattenDeep(_.map(triggers, (trigger) => _.map(trigger.groups, 'name')))); + const groups = _.uniq(_.flattenDeep(_.map(triggers, (trigger) => _.map(trigger.groups, 'name')))); // let severity = _.map(c.TRIGGER_SEVERITY, 'text'); - let stats = {}; + const stats = {}; _.each(groups, (group) => { - stats[group] = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0}; // severity:count + stats[group] = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0}; // severity:count }); _.each(triggers, (trigger) => { _.each(trigger.groups, (group) => { @@ -205,7 +221,7 @@ function convertHistoryPoint(point) { } function convertTrendPoint(valueType, point) { - var value; + let value; switch (valueType) { case "min": value = point.value_min; @@ -242,6 +258,3 @@ export default { handleTriggersResponse, sortTimeseries }; - -// Fix for backward compatibility with lodash 2.4 -if (!_.uniqBy) {_.uniqBy = _.uniq;} diff --git a/src/datasource-zabbix/specs/datasource.spec.js b/src/datasource-zabbix/specs/datasource.spec.js index a51a06b..5ac2f85 100644 --- a/src/datasource-zabbix/specs/datasource.spec.js +++ b/src/datasource-zabbix/specs/datasource.spec.js @@ -4,6 +4,12 @@ import { Datasource } from "../module"; import { zabbixTemplateFormat } from "../datasource"; import { dateMath } from '@grafana/data'; +jest.mock('@grafana/runtime', () => ({ + getBackendSrv: () => ({ + datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}), + }), +}), {virtual: true}); + describe('ZabbixDatasource', () => { let ctx = {}; @@ -21,11 +27,11 @@ describe('ZabbixDatasource', () => { }; ctx.templateSrv = mocks.templateSrvMock; - ctx.backendSrv = mocks.backendSrvMock; + // ctx.backendSrv = mocks.backendSrvMock; ctx.datasourceSrv = mocks.datasourceSrvMock; ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock; - ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.backendSrv, ctx.datasourceSrv, ctx.zabbixAlertingSrv); + ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.zabbixAlertingSrv); }); describe('When querying data', () => { @@ -119,7 +125,7 @@ describe('ZabbixDatasource', () => { item: {filter: "System information"}, textFilter: "", useCaptureGroups: true, - mode: 2, + queryType: 2, resultFormat: "table", options: { skipEmptyValues: false diff --git a/src/datasource-zabbix/specs/dbConnector.test.js b/src/datasource-zabbix/specs/dbConnector.test.ts similarity index 51% rename from src/datasource-zabbix/specs/dbConnector.test.js rename to src/datasource-zabbix/specs/dbConnector.test.ts index b41276c..8233708 100644 --- a/src/datasource-zabbix/specs/dbConnector.test.js +++ b/src/datasource-zabbix/specs/dbConnector.test.ts @@ -1,11 +1,17 @@ -import mocks from '../../test-setup/mocks'; import { DBConnector } from '../zabbix/connectors/dbConnector'; +const loadDatasourceMock = jest.fn().mockResolvedValue({ id: 42, name: 'foo', meta: {} }); +const getAllMock = jest.fn().mockReturnValue([{ id: 42, name: 'foo', meta: {} }]); + +jest.mock('@grafana/runtime', () => ({ + getDataSourceSrv: () => ({ + loadDatasource: loadDatasourceMock, + getAll: getAllMock + }), +})); + describe('DBConnector', () => { - let ctx = {}; - const datasourceSrv = mocks.datasourceSrvMock; - datasourceSrv.loadDatasource.mockResolvedValue({ id: 42, name: 'foo', meta: {} }); - datasourceSrv.getAll.mockReturnValue([{ id: 42, name: 'foo' }]); + const ctx: any = {}; describe('When init DB connector', () => { beforeEach(() => { @@ -13,34 +19,34 @@ describe('DBConnector', () => { datasourceId: 42, datasourceName: undefined }; + + loadDatasourceMock.mockClear(); + getAllMock.mockClear(); }); it('should try to load datasource by name first', () => { - ctx.options = { - datasourceName: 'bar' - }; - const dbConnector = new DBConnector(ctx.options, datasourceSrv); + const dbConnector = new DBConnector({ datasourceName: 'bar' }); dbConnector.loadDBDataSource(); - expect(datasourceSrv.getAll).not.toHaveBeenCalled(); - expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('bar'); + expect(getAllMock).not.toHaveBeenCalled(); + expect(loadDatasourceMock).toHaveBeenCalledWith('bar'); }); it('should load datasource by id if name not present', () => { - const dbConnector = new DBConnector(ctx.options, datasourceSrv); + const dbConnector = new DBConnector({ datasourceId: 42 }); dbConnector.loadDBDataSource(); - expect(datasourceSrv.getAll).toHaveBeenCalled(); - expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('foo'); + expect(getAllMock).toHaveBeenCalled(); + expect(loadDatasourceMock).toHaveBeenCalledWith('foo'); }); it('should throw error if no name and id specified', () => { ctx.options = {}; - const dbConnector = new DBConnector(ctx.options, datasourceSrv); + const dbConnector = new DBConnector(ctx.options); return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified'); }); it('should throw error if datasource with given id is not found', () => { ctx.options.datasourceId = 45; - const dbConnector = new DBConnector(ctx.options, datasourceSrv); + const dbConnector = new DBConnector(ctx.options); return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found'); }); }); diff --git a/src/datasource-zabbix/specs/influxdbConnector.test.js b/src/datasource-zabbix/specs/influxdbConnector.test.js index a259b6a..6e39913 100644 --- a/src/datasource-zabbix/specs/influxdbConnector.test.js +++ b/src/datasource-zabbix/specs/influxdbConnector.test.js @@ -1,17 +1,20 @@ import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector'; import { compactQuery } from '../utils'; +jest.mock('@grafana/runtime', () => ({ + getDataSourceSrv: jest.fn(() => ({ + loadDatasource: jest.fn().mockResolvedValue( + { id: 42, name: 'InfluxDB DS', meta: {} } + ), + })), +})); + describe('InfluxDBConnector', () => { let ctx = {}; beforeEach(() => { ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' }; - ctx.datasourceSrvMock = { - loadDatasource: jest.fn().mockResolvedValue( - { id: 42, name: 'InfluxDB DS', meta: {} } - ), - }; - ctx.influxDBConnector = new InfluxDBConnector(ctx.options, ctx.datasourceSrvMock); + ctx.influxDBConnector = new InfluxDBConnector(ctx.options); ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]); ctx.defaultQueryParams = { itemids: ['123', '234'], diff --git a/src/datasource-zabbix/timeseries.js b/src/datasource-zabbix/timeseries.ts similarity index 87% rename from src/datasource-zabbix/timeseries.js rename to src/datasource-zabbix/timeseries.ts index 109fa41..c40ee91 100644 --- a/src/datasource-zabbix/timeseries.js +++ b/src/datasource-zabbix/timeseries.ts @@ -20,35 +20,30 @@ const POINT_TIMESTAMP = 1; * Downsample time series by using given function (avg, min, max). */ function downsample(datapoints, time_to, ms_interval, func) { - var downsampledSeries = []; - var timeWindow = { + const downsampledSeries = []; + const timeWindow = { from: time_to * 1000 - ms_interval, to: time_to * 1000 }; - var points_sum = 0; - var points_num = 0; - var value_avg = 0; - var frame = []; + let points_sum = 0; + let points_num = 0; + let value_avg = 0; + let frame = []; - for (var i = datapoints.length - 1; i >= 0; i -= 1) { + for (let i = datapoints.length - 1; i >= 0; i -= 1) { if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) { points_sum += datapoints[i][0]; points_num++; frame.push(datapoints[i][0]); - } - else { + } else { value_avg = points_num ? points_sum / points_num : 0; if (func === "max") { downsampledSeries.push([_.max(frame), timeWindow.to]); - } - else if (func === "min") { + } else if (func === "min") { downsampledSeries.push([_.min(frame), timeWindow.to]); - } - - // avg by default - else { + } else { downsampledSeries.push([value_avg, timeWindow.to]); } @@ -72,25 +67,25 @@ function downsample(datapoints, time_to, ms_interval, func) { * datapoints: [[, ], ...] */ function groupBy(datapoints, interval, groupByCallback) { - var ms_interval = utils.parseInterval(interval); + const ms_interval = utils.parseInterval(interval); // Calculate frame timestamps - var frames = _.groupBy(datapoints, function (point) { + const frames = _.groupBy(datapoints, point => { // Calculate time for group of points return Math.floor(point[1] / ms_interval) * ms_interval; }); // frame: { '': [[, ], ...] } // return [{ '': }, { '': }, ...] - var grouped = _.mapValues(frames, function (frame) { - var points = _.map(frame, function (point) { + const grouped = _.mapValues(frames, frame => { + const points = _.map(frame, point => { return point[0]; }); return groupByCallback(points); }); // Convert points to Grafana format - return sortByTime(_.map(grouped, function (value, timestamp) { + return sortByTime(_.map(grouped, (value, timestamp) => { return [Number(value), Number(timestamp)]; })); } @@ -104,15 +99,15 @@ export function groupBy_perf(datapoints, interval, groupByCallback) { return groupByRange(datapoints, groupByCallback); } - let ms_interval = utils.parseInterval(interval); - let grouped_series = []; + const ms_interval = utils.parseInterval(interval); + const grouped_series = []; let frame_values = []; let frame_value; let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0; let point_frame_ts = frame_ts; let point; - for (let i=0; i < datapoints.length; i++) { + for (let i = 0; i < datapoints.length; i++) { point = datapoints[i]; point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval); if (point_frame_ts === frame_ts) { @@ -142,7 +137,7 @@ export function groupByRange(datapoints, groupByCallback) { const frame_start = datapoints[0][POINT_TIMESTAMP]; const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP]; let point; - for (let i=0; i < datapoints.length; i++) { + for (let i = 0; i < datapoints.length; i++) { point = datapoints[i]; frame_values.push(point[POINT_VALUE]); } @@ -157,30 +152,30 @@ export function groupByRange(datapoints, groupByCallback) { function sumSeries(timeseries) { // Calculate new points for interpolation - var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function (point) { + let new_timestamps = _.uniq(_.map(_.flatten(timeseries), point => { return point[1]; })); new_timestamps = _.sortBy(new_timestamps); - var interpolated_timeseries = _.map(timeseries, function (series) { + const interpolated_timeseries = _.map(timeseries, series => { series = fillZeroes(series, new_timestamps); - var timestamps = _.map(series, function (point) { + const timestamps = _.map(series, point => { return point[1]; }); - var new_points = _.map(_.difference(new_timestamps, timestamps), function (timestamp) { + const new_points = _.map(_.difference(new_timestamps, timestamps), timestamp => { return [null, timestamp]; }); - var new_series = series.concat(new_points); + const new_series = series.concat(new_points); return sortByTime(new_series); }); _.each(interpolated_timeseries, interpolateSeries); - var new_timeseries = []; - var sum; - for (var i = new_timestamps.length - 1; i >= 0; i--) { + const new_timeseries = []; + let sum; + for (let i = new_timestamps.length - 1; i >= 0; i--) { sum = 0; - for (var j = interpolated_timeseries.length - 1; j >= 0; j--) { + for (let j = interpolated_timeseries.length - 1; j >= 0; j--) { sum += interpolated_timeseries[j][i][0]; } new_timeseries.push([sum, new_timestamps[i]]); @@ -225,9 +220,9 @@ function offset(datapoints, delta) { * @param {*} datapoints */ function delta(datapoints) { - let newSeries = []; + const newSeries = []; let deltaValue; - for (var i = 1; i < datapoints.length; i++) { + for (let i = 1; i < datapoints.length; i++) { deltaValue = datapoints[i][0] - datapoints[i - 1][0]; newSeries.push([deltaValue, datapoints[i][1]]); } @@ -239,7 +234,7 @@ function delta(datapoints) { * @param {*} datapoints */ function rate(datapoints) { - let newSeries = []; + const newSeries = []; let point, point_prev; let valueDelta = 0; let timeDelta = 0; @@ -261,7 +256,7 @@ function rate(datapoints) { } function simpleMovingAverage(datapoints, n) { - let sma = []; + const sma = []; let w_sum; let w_avg = null; let w_count = 0; @@ -352,7 +347,7 @@ function expMovingAverage(datapoints, n) { } function PERCENTILE(n, values) { - var sorted = _.sortBy(values); + const sorted = _.sortBy(values); return sorted[Math.floor(sorted.length * n / 100)]; } @@ -361,7 +356,7 @@ function COUNT(values) { } function SUM(values) { - var sum = null; + let sum = null; for (let i = 0; i < values.length; i++) { if (values[i] !== null) { sum += values[i]; @@ -371,7 +366,7 @@ function SUM(values) { } function AVERAGE(values) { - let values_non_null = getNonNullValues(values); + const values_non_null = getNonNullValues(values); if (values_non_null.length === 0) { return null; } @@ -379,7 +374,7 @@ function AVERAGE(values) { } function getNonNullValues(values) { - let values_non_null = []; + const values_non_null = []; for (let i = 0; i < values.length; i++) { if (values[i] !== null) { values_non_null.push(values[i]); @@ -397,7 +392,7 @@ function MAX(values) { } function MEDIAN(values) { - var sorted = _.sortBy(values); + const sorted = _.sortBy(values); return sorted[Math.floor(sorted.length / 2)]; } @@ -418,7 +413,7 @@ function getPointTimeFrame(timestamp, ms_interval) { } function sortByTime(series) { - return _.sortBy(series, function (point) { + return _.sortBy(series, point => { return point[1]; }); } @@ -432,8 +427,8 @@ function sortByTime(series) { * @param {*} timestamps */ function fillZeroes(series, timestamps) { - let prepend = []; - let append = []; + const prepend = []; + const append = []; let new_point; for (let i = 0; i < timestamps.length; i++) { if (timestamps[i] < series[0][POINT_TIMESTAMP]) { @@ -451,10 +446,10 @@ function fillZeroes(series, timestamps) { * Interpolate series with gaps */ function interpolateSeries(series) { - var left, right; + let left, right; // Interpolate series - for (var i = series.length - 1; i >= 0; i--) { + for (let i = series.length - 1; i >= 0; i--) { if (!series[i][0]) { left = findNearestLeft(series, i); right = findNearestRight(series, i); @@ -479,7 +474,7 @@ function linearInterpolation(timestamp, left, right) { } function findNearestRight(series, pointIndex) { - for (var i = pointIndex; i < series.length; i++) { + for (let i = pointIndex; i < series.length; i++) { if (series[i][0] !== null) { return series[i]; } @@ -488,7 +483,7 @@ function findNearestRight(series, pointIndex) { } function findNearestLeft(series, pointIndex) { - for (var i = pointIndex; i > 0; i--) { + for (let i = pointIndex; i > 0; i--) { if (series[i][0] !== null) { return series[i]; } diff --git a/src/datasource-zabbix/types.ts b/src/datasource-zabbix/types.ts index ef2e384..3c4ade6 100644 --- a/src/datasource-zabbix/types.ts +++ b/src/datasource-zabbix/types.ts @@ -111,4 +111,183 @@ export enum VariableQueryTypes { Host = 'host', Application = 'application', Item = 'item', + ItemValues = 'itemValues', +} + +export enum ShowProblemTypes { + Problems = 'problems', + Recent = 'recent', + History = 'history', +} + +export interface ProblemDTO { + triggerid?: string; + eventid?: string; + timestamp: number; + + /** Name of the trigger. */ + name?: string; + + /** Same as a name. */ + description?: string; + + /** Whether the trigger is in OK or problem state. */ + value?: string; + + datasource?: string; + comments?: string; + host?: string; + hostTechName?: string; + proxy?: string; + severity?: string; + + acknowledged?: '1' | '0'; + acknowledges?: ZBXAcknowledge[]; + + groups?: ZBXGroup[]; + hosts?: ZBXHost[]; + items?: ZBXItem[]; + alerts?: ZBXAlert[]; + tags?: ZBXTag[]; + url?: string; + + expression?: string; + correlation_mode?: string; + correlation_tag?: string; + suppressed?: string; + suppression_data?: any[]; + state?: string; + maintenance?: boolean; + manual_close?: string; + error?: string; + + showAckButton?: boolean; +} + +export interface ZBXProblem { + acknowledged?: '1' | '0'; + acknowledges?: ZBXAcknowledge[]; + clock: string; + ns: string; + correlationid?: string; + datasource?: string; + name?: string; + eventid?: string; + maintenance?: boolean; + object?: string; + objectid?: string; + opdata?: any; + r_eventid?: string; + r_clock?: string; + r_ns?: string; + severity?: string; + showAckButton?: boolean; + source?: string; + suppressed?: string; + suppression_data?: any[]; + tags?: ZBXTag[]; + userid?: string; +} + +export interface ZBXTrigger { + acknowledges?: ZBXAcknowledge[]; + showAckButton?: boolean; + alerts?: ZBXAlert[]; + age?: string; + color?: string; + comments?: string; + correlation_mode?: string; + correlation_tag?: string; + datasource?: string; + description?: string; + error?: string; + expression?: string; + flags?: string; + groups?: ZBXGroup[]; + host?: string; + hostTechName?: string; + hosts?: ZBXHost[]; + items?: ZBXItem[]; + lastEvent?: ZBXEvent; + lastchange?: string; + lastchangeUnix?: number; + maintenance?: boolean; + manual_close?: string; + priority?: string; + proxy?: string; + recovery_expression?: string; + recovery_mode?: string; + severity?: string; + state?: string; + status?: string; + tags?: ZBXTag[]; + templateid?: string; + triggerid?: string; + /** Whether the trigger can generate multiple problem events. */ + type?: string; + url?: string; + value?: string; +} + +export interface ZBXGroup { + groupid: string; + name: string; +} + +export interface ZBXHost { + hostid: string; + name: string; + host: string; + maintenance_status?: string; + proxy_hostid?: string; +} + +export interface ZBXItem { + itemid: string; + name: string; + key_: string; + lastvalue?: string; +} + +export interface ZBXEvent { + eventid: string; + clock: string; + ns?: string; + value?: string; + name?: string; + source?: string; + object?: string; + objectid?: string; + severity?: string; + hosts?: ZBXHost[]; + acknowledged?: '1' | '0'; + acknowledges?: ZBXAcknowledge[]; + tags?: ZBXTag[]; + suppressed?: string; +} + +export interface ZBXTag { + tag: string; + value?: string; +} + +export interface ZBXAcknowledge { + acknowledgeid: string; + eventid: string; + userid: string; + action: string; + clock: string; + time: string; + message?: string; + user: string; + alias: string; + name: string; + surname: string; +} + +export interface ZBXAlert { + eventid: string; + clock: string; + message: string; + error: string; } diff --git a/src/datasource-zabbix/utils.ts b/src/datasource-zabbix/utils.ts index 44bc5a5..c5ba1e9 100644 --- a/src/datasource-zabbix/utils.ts +++ b/src/datasource-zabbix/utils.ts @@ -19,7 +19,7 @@ export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\ * @param {string} key item key, ie system.cpu.util[,system,avg1] * @return {string} expanded name, ie "CPU system time" */ -export function expandItemName(name, key) { +export function expandItemName(name: string, key: string): string { // extract params from key: // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"] @@ -78,13 +78,26 @@ export function containsMacro(itemName) { return MACRO_PATTERN.test(itemName); } -export function replaceMacro(item, macros) { - let itemName = item.name; +export function replaceMacro(item, macros, isTriggerItem?) { + let itemName = isTriggerItem ? item.url : item.name; const item_macros = itemName.match(MACRO_PATTERN); _.forEach(item_macros, macro => { const host_macros = _.filter(macros, m => { if (m.hostid) { - return m.hostid === item.hostid; + if (isTriggerItem) { + // Trigger item can have multiple hosts + // Check all trigger host ids against macro host id + let hostIdFound = false; + _.forEach(item.hosts, h => { + if (h.hostid === m.hostid) { + hostIdFound = true; + } + }); + return hostIdFound; + } else { + // Check app host id against macro host id + return m.hostid === item.hostid; + } } else { // Add global macros return true; @@ -222,10 +235,11 @@ export function escapeRegex(value) { return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&'); } -export function parseInterval(interval) { +export function parseInterval(interval: string): number { const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g; const momentInterval: any[] = intervalPattern.exec(interval); - return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf(); + const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]); + return (duration.valueOf() as number); } export function parseTimeShiftInterval(interval) { @@ -314,7 +328,7 @@ export function isValidVersion(version) { return versionPattern.exec(version); } -export function parseVersion(version) { +export function parseVersion(version: string) { const match = versionPattern.exec(version); if (!match) { return null; @@ -344,7 +358,29 @@ export function getArrayDepth(a, level = 0) { return level + 1; } -// Fix for backward compatibility with lodash 2.4 -if (!_.includes) { - _.includes = (_ as any).contains; +/** + * Checks whether its argument represents a numeric value. + */ +export function isNumeric(n: any): boolean { + return !isNaN(parseFloat(n)) && isFinite(n); +} + +/** + * Parses tags string into array of {tag: value} objects + */ +export function parseTags(tagStr: string): any[] { + if (!tagStr) { + return []; + } + + let tags: any[] = _.map(tagStr.split(','), (tag) => tag.trim()); + tags = _.map(tags, (tag) => { + const tagParts = tag.split(':'); + return {tag: tagParts[0].trim(), value: tagParts[1].trim()}; + }); + return tags; +} + +export function mustArray(result: any): any[] { + return result || []; } diff --git a/src/datasource-zabbix/zabbix/connectors/dbConnector.js b/src/datasource-zabbix/zabbix/connectors/dbConnector.js index c4039a1..e118aab 100644 --- a/src/datasource-zabbix/zabbix/connectors/dbConnector.js +++ b/src/datasource-zabbix/zabbix/connectors/dbConnector.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { getDataSourceSrv } from '@grafana/runtime'; export const DEFAULT_QUERY_LIMIT = 10000; export const HISTORY_TO_TABLE_MAP = { @@ -34,31 +35,30 @@ export const consolidateByTrendColumns = { * `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API. */ export class DBConnector { - constructor(options, datasourceSrv) { - this.datasourceSrv = datasourceSrv; + constructor(options) { this.datasourceId = options.datasourceId; this.datasourceName = options.datasourceName; this.datasourceTypeId = null; this.datasourceTypeName = null; } - static loadDatasource(dsId, dsName, datasourceSrv) { + static loadDatasource(dsId, dsName) { if (!dsName && dsId !== undefined) { - let ds = _.find(datasourceSrv.getAll(), {'id': dsId}); + let ds = _.find(getDataSourceSrv().getAll(), {'id': dsId}); if (!ds) { return Promise.reject(`Data Source with ID ${dsId} not found`); } dsName = ds.name; } if (dsName) { - return datasourceSrv.loadDatasource(dsName); + return getDataSourceSrv().loadDatasource(dsName); } else { return Promise.reject(`Data Source name should be specified`); } } loadDBDataSource() { - return DBConnector.loadDatasource(this.datasourceId, this.datasourceName, this.datasourceSrv) + return DBConnector.loadDatasource(this.datasourceId, this.datasourceName) .then(ds => { this.datasourceTypeId = ds.meta.id; this.datasourceTypeName = ds.meta.name; @@ -123,22 +123,36 @@ export class ZabbixNotImplemented { */ function convertGrafanaTSResponse(time_series, items, addHostName) { //uniqBy is needed to deduplicate - var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); + const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); let grafanaSeries = _.map(_.compact(time_series), series => { - let itemid = series.name; - var item = _.find(items, {'itemid': itemid}); - var alias = item.name; - //only when actual multi hosts selected - if (_.keys(hosts).length > 1 && addHostName) { - var host = _.find(hosts, {'hostid': item.hostid}); - alias = host.name + ": " + alias; + const itemid = series.name; + const item = _.find(items, {'itemid': itemid}); + let alias = item.name; + + // Add scopedVars for using in alias functions + const scopedVars = { + '__zbx_item': { value: item.name }, + '__zbx_item_name': { value: item.name }, + '__zbx_item_key': { value: item.key_ }, + }; + + if (_.keys(hosts).length > 0) { + const host = _.find(hosts, {'hostid': item.hostid}); + scopedVars['__zbx_host'] = { value: host.host }; + scopedVars['__zbx_host_name'] = { value: host.name }; + + // Only add host when multiple hosts selected + if (_.keys(hosts).length > 1 && addHostName) { + alias = host.name + ": " + alias; + } } // CachingProxy deduplicates requests and returns one time series for equal queries. // Clone is needed to prevent changing of series object shared between all targets. - let datapoints = _.cloneDeep(series.points); + const datapoints = _.cloneDeep(series.points); return { target: alias, - datapoints: datapoints + datapoints, + scopedVars, }; }); diff --git a/src/datasource-zabbix/zabbix/connectors/influxdb/influxdbConnector.js b/src/datasource-zabbix/zabbix/connectors/influxdb/influxdbConnector.js index 512f2da..4bd5012 100644 --- a/src/datasource-zabbix/zabbix/connectors/influxdb/influxdbConnector.js +++ b/src/datasource-zabbix/zabbix/connectors/influxdb/influxdbConnector.js @@ -11,8 +11,8 @@ const consolidateByFunc = { }; export class InfluxDBConnector extends DBConnector { - constructor(options, datasourceSrv) { - super(options, datasourceSrv); + constructor(options) { + super(options); this.retentionPolicy = options.retentionPolicy; super.loadDBDataSource().then(ds => { this.influxDS = ds; @@ -24,7 +24,14 @@ export class InfluxDBConnector extends DBConnector { * Try to invoke test query for one of Zabbix database tables. */ testDataSource() { - return this.influxDS.testDatasource(); + return this.influxDS.testDatasource().then(result => { + if (result.status && result.status === 'error') { + return Promise.reject({ data: { + message: `InfluxDB connection error: ${result.message}` + }}); + } + return result; + }); } getHistory(items, timeFrom, timeTill, options) { diff --git a/src/datasource-zabbix/zabbix/connectors/sql/mysql.js b/src/datasource-zabbix/zabbix/connectors/sql/mysql.js index 31f0efa..a49cbe3 100644 --- a/src/datasource-zabbix/zabbix/connectors/sql/mysql.js +++ b/src/datasource-zabbix/zabbix/connectors/sql/mysql.js @@ -3,26 +3,24 @@ */ function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { - let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`; let query = ` - SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(value) AS value + SELECT CAST(itemid AS CHAR) AS metric, MIN(clock) AS time_sec, ${aggFunction}(value) AS value FROM ${table} WHERE itemid IN (${itemids}) AND clock > ${timeFrom} AND clock < ${timeTill} - GROUP BY ${time_expression}, metric + GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric ORDER BY time_sec ASC `; return query; } function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) { - let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`; let query = ` - SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(${valueColumn}) AS value + SELECT CAST(itemid AS CHAR) AS metric, MIN(clock) AS time_sec, ${aggFunction}(${valueColumn}) AS value FROM ${table} WHERE itemid IN (${itemids}) AND clock > ${timeFrom} AND clock < ${timeTill} - GROUP BY ${time_expression}, metric + GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric ORDER BY time_sec ASC `; return query; diff --git a/src/datasource-zabbix/zabbix/connectors/sql/sqlConnector.js b/src/datasource-zabbix/zabbix/connectors/sql/sqlConnector.js index 0ec5dc6..da48708 100644 --- a/src/datasource-zabbix/zabbix/connectors/sql/sqlConnector.js +++ b/src/datasource-zabbix/zabbix/connectors/sql/sqlConnector.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { getBackendSrv } from '@grafana/runtime'; import { compactQuery } from '../../../utils'; import mysql from './mysql'; import postgres from './postgres'; @@ -10,15 +11,14 @@ const supportedDatabases = { }; export class SQLConnector extends DBConnector { - constructor(options, datasourceSrv) { - super(options, datasourceSrv); + constructor(options) { + super(options); this.limit = options.limit || DEFAULT_QUERY_LIMIT; this.sqlDialect = null; super.loadDBDataSource() - .then(ds => { - this.backendSrv = ds.backendSrv; + .then(() => { this.loadSQLDialect(); }); } @@ -43,6 +43,12 @@ export class SQLConnector extends DBConnector { let {intervalMs, consolidateBy} = options; let intervalSec = Math.ceil(intervalMs / 1000); + // The interval must match the time range exactly n times, otherwise + // the resulting first and last data points will yield invalid values in the + // calculated average value in downsampleSeries - when using consolidateBy(avg) + let numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec); + intervalSec = (timeTill - timeFrom) / numOfIntervals; + consolidateBy = consolidateBy || 'avg'; let aggFunction = dbConnector.consolidateByFunc[consolidateBy]; @@ -66,6 +72,12 @@ export class SQLConnector extends DBConnector { let { intervalMs, consolidateBy } = options; let intervalSec = Math.ceil(intervalMs / 1000); + // The interval must match the time range exactly n times, otherwise + // the resulting first and last data points will yield invalid values in the + // calculated average value in downsampleSeries - when using consolidateBy(avg) + let numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec); + intervalSec = (timeTill - timeFrom) / numOfIntervals; + consolidateBy = consolidateBy || 'avg'; let aggFunction = dbConnector.consolidateByFunc[consolidateBy]; @@ -96,7 +108,7 @@ export class SQLConnector extends DBConnector { maxDataPoints: this.limit }; - return this.backendSrv.datasourceRequest({ + return getBackendSrv().datasourceRequest({ url: '/api/tsdb/query', method: 'POST', data: { diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts new file mode 100644 index 0000000..007d592 --- /dev/null +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts @@ -0,0 +1,42 @@ +export interface JSONRPCRequest { + jsonrpc: '2.0' | string; + method: string; + id: number; + auth?: string | null; + params?: JSONRPCRequestParams; +} + +export interface JSONRPCResponse { + jsonrpc: '2.0' | string; + id: number; + result?: T; + error?: JSONRPCError; +} + +export interface JSONRPCError { + code?: number; + message?: string; + data?: string; +} + +export interface GFHTTPRequest { + method: HTTPMethod; + url: string; + data?: any; + headers?: {[key: string]: string}; + withCredentials?: boolean; +} + +export type JSONRPCRequestParams = {[key: string]: any}; + +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE'; + +export type GFRequestOptions = {[key: string]: any}; + +export interface ZabbixRequestResponse { + data?: JSONRPCResponse; +} + +export type ZabbixAPIResponse = T; + +export type APILoginResponse = string; diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts similarity index 61% rename from src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js rename to src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts index f344d03..aa4104e 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts @@ -1,8 +1,14 @@ import _ from 'lodash'; +import semver from 'semver'; import kbn from 'grafana/app/core/utils/kbn'; import * as utils from '../../../utils'; import { ZabbixAPICore } from './zabbixAPICore'; import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; +import { ShowProblemTypes, ZBXProblem } from '../../../types'; +import { JSONRPCRequestParams } from './types'; +import { getBackendSrv } from '@grafana/runtime'; + +const DEFAULT_ZABBIX_VERSION = '3.0.0'; /** * Zabbix API Wrapper. @@ -10,12 +16,25 @@ import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MI * Wraps API calls and provides high-level methods. */ export class ZabbixAPIConnector { - constructor(api_url, username, password, version, basicAuth, withCredentials, backendSrv, datasourceId) { + url: string; + username: string; + password: string; + auth: string; + requestOptions: { basicAuth: any; withCredentials: boolean; }; + loginPromise: Promise; + loginErrorCount: number; + maxLoginAttempts: number; + zabbixAPICore: ZabbixAPICore; + getTrend: (items: any, timeFrom: any, timeTill: any) => Promise; + version: string; + getVersionPromise: Promise; + datasourceId: number; + + constructor(api_url: string, username: string, password: string, basicAuth: any, withCredentials: boolean, datasourceId: number) { this.url = api_url; this.username = username; this.password = password; this.auth = ''; - this.version = version; this.requestOptions = { basicAuth: basicAuth, @@ -23,16 +42,17 @@ export class ZabbixAPIConnector { }; this.datasourceId = datasourceId; - this.backendSrv = backendSrv; this.loginPromise = null; this.loginErrorCount = 0; this.maxLoginAttempts = 3; - this.zabbixAPICore = new ZabbixAPICore(backendSrv); + this.zabbixAPICore = new ZabbixAPICore(); this.getTrend = this.getTrend_ZBXNEXT1193; //getTrend = getTrend_30; + + this.initVersion(); } ////////////////////////// @@ -59,13 +79,40 @@ export class ZabbixAPIConnector { }], }; - return this.backendSrv.datasourceRequest({ + return getBackendSrv().datasourceRequest({ url: '/api/tsdb/query', method: 'POST', data: tsdbRequestData }); } + _request(method: string, params: JSONRPCRequestParams): Promise { + if (!this.version) { + return this.initVersion().then(() => this.request(method, params)); + } + + return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth) + .catch(error => { + if (isNotInitialized(error.data)) { + // If API not initialized yet (auth is empty), login first + return this.loginOnce() + .then(() => this.request(method, params)); + } else if (isNotAuthorized(error.data)) { + // Handle auth errors + this.loginErrorCount++; + if (this.loginErrorCount > this.maxLoginAttempts) { + this.loginErrorCount = 0; + return Promise.resolve(); + } else { + return this.loginOnce() + .then(() => this.request(method, params)); + } + } else { + return Promise.reject(error); + } + }); + } + handleTsdbResponse(response) { if (!response || !response.data || !response.data.results) { return []; @@ -78,9 +125,8 @@ export class ZabbixAPIConnector { * When API unauthenticated or auth token expired each request produce login() * call. But auth token is common to all requests. This function wraps login() method * and call it once. If login() already called just wait for it (return its promise). - * @return login promise */ - loginOnce() { + loginOnce(): Promise { if (!this.loginPromise) { this.loginPromise = Promise.resolve( this.login().then(auth => { @@ -96,7 +142,7 @@ export class ZabbixAPIConnector { /** * Get authentication token. */ - login() { + login(): Promise { return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions); } @@ -107,23 +153,49 @@ export class ZabbixAPIConnector { return this.zabbixAPICore.getVersion(this.url, this.requestOptions); } + initVersion(): Promise { + if (!this.getVersionPromise) { + this.getVersionPromise = Promise.resolve( + this.getVersion().then(version => { + if (version) { + console.log(`Zabbix version detected: ${version}`); + } else { + console.log(`Failed to detect Zabbix version, use default ${DEFAULT_ZABBIX_VERSION}`); + } + + this.version = version || DEFAULT_ZABBIX_VERSION; + this.getVersionPromise = null; + return version; + }) + ); + } + return this.getVersionPromise; + } + //////////////////////////////// // Zabbix API method wrappers // //////////////////////////////// - acknowledgeEvent(eventid, message) { - const action = this.version >= 4 ? ZBX_ACK_ACTION_ACK + ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE; - const params = { + acknowledgeEvent(eventid: string, message: string, action?: number, severity?: number) { + if (!action) { + action = semver.gte(this.version, '4.0.0') ? ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE; + } + + const params: any = { eventids: eventid, message: message, action: action }; + if (severity) { + params.severity = severity; + } + return this.request('event.acknowledge', params); } getGroups() { - var params = { + const params = { output: ['name'], sortfield: 'name', real_hosts: true @@ -133,7 +205,7 @@ export class ZabbixAPIConnector { } getHosts(groupids) { - var params = { + const params: any = { output: ['name', 'host'], sortfield: 'name' }; @@ -144,8 +216,8 @@ export class ZabbixAPIConnector { return this.request('host.get', params); } - getApps(hostids) { - var params = { + getApps(hostids): Promise { + const params = { output: 'extend', hostids: hostids }; @@ -161,7 +233,7 @@ export class ZabbixAPIConnector { * @return {[type]} array of items */ getItems(hostids, appids, itemtype) { - var params = { + const params: any = { output: [ 'name', 'key_', 'value_type', @@ -172,7 +244,7 @@ export class ZabbixAPIConnector { sortfield: 'name', webitems: true, filter: {}, - selectHosts: ['hostid', 'name'] + selectHosts: ['hostid', 'name', 'host'] }; if (hostids) { params.hostids = hostids; @@ -194,7 +266,7 @@ export class ZabbixAPIConnector { } getItemsByIDs(itemids) { - var params = { + const params = { itemids: itemids, output: [ 'name', 'key_', @@ -208,11 +280,11 @@ export class ZabbixAPIConnector { }; return this.request('item.get', params) - .then(utils.expandItems); + .then(items => utils.expandItems(items)); } getMacros(hostids) { - var params = { + const params = { output: 'extend', hostids: hostids }; @@ -221,7 +293,7 @@ export class ZabbixAPIConnector { } getGlobalMacros() { - var params = { + const params = { output: 'extend', globalmacro: true }; @@ -230,7 +302,7 @@ export class ZabbixAPIConnector { } getLastValue(itemid) { - var params = { + const params = { output: ['lastvalue'], itemids: itemid }; @@ -249,10 +321,10 @@ export class ZabbixAPIConnector { getHistory(items, timeFrom, timeTill) { // Group items by value type and perform request for each value type - let grouped_items = _.groupBy(items, 'value_type'); - let promises = _.map(grouped_items, (items, value_type) => { - let itemids = _.map(items, 'itemid'); - let params = { + const grouped_items = _.groupBy(items, 'value_type'); + const promises = _.map(grouped_items, (items, value_type) => { + const itemids = _.map(items, 'itemid'); + const params: any = { output: 'extend', history: value_type, itemids: itemids, @@ -284,10 +356,10 @@ export class ZabbixAPIConnector { getTrend_ZBXNEXT1193(items, timeFrom, timeTill) { // Group items by value type and perform request for each value type - let grouped_items = _.groupBy(items, 'value_type'); - let promises = _.map(grouped_items, (items, value_type) => { - let itemids = _.map(items, 'itemid'); - let params = { + const grouped_items = _.groupBy(items, 'value_type'); + const promises = _.map(grouped_items, (items, value_type) => { + const itemids = _.map(items, 'itemid'); + const params: any = { output: 'extend', trend: value_type, itemids: itemids, @@ -308,10 +380,10 @@ export class ZabbixAPIConnector { } getTrend_30(items, time_from, time_till, value_type) { - var self = this; - var itemids = _.map(items, 'itemid'); + const self = this; + const itemids = _.map(items, 'itemid'); - var params = { + const params: any = { output: ["itemid", "clock", value_type @@ -328,8 +400,8 @@ export class ZabbixAPIConnector { return self.request('trend.get', params); } - getITService(serviceids) { - var params = { + getITService(serviceids?) { + const params = { output: 'extend', serviceids: serviceids }; @@ -337,18 +409,88 @@ export class ZabbixAPIConnector { } getSLA(serviceids, timeRange, options) { - const intervals = buildSLAIntervals(timeRange, options.intervalMs); - const params = { + const [timeFrom, timeTo] = timeRange; + let intervals = [{ from: timeFrom, to: timeTo }]; + if (options.slaInterval === 'auto') { + const interval = getSLAInterval(options.intervalMs); + intervals = buildSLAIntervals(timeRange, interval); + } else if (options.slaInterval !== 'none') { + const interval = utils.parseInterval(options.slaInterval) / 1000; + intervals = buildSLAIntervals(timeRange, interval); + } + + const params: any = { serviceids, intervals }; + return this.request('service.getsla', params); } - getTriggers(groupids, hostids, applicationids, options) { - let {showTriggers, maintenance, timeFrom, timeTo} = options; + getProblems(groupids, hostids, applicationids, options): Promise { + const { timeFrom, timeTo, recent, severities, limit, acknowledged } = options; - let params = { + const params: any = { + output: 'extend', + selectAcknowledges: 'extend', + selectSuppressionData: 'extend', + selectTags: 'extend', + source: '0', + object: '0', + sortfield: ['eventid'], + sortorder: 'ASC', + evaltype: '0', + // preservekeys: '1', + groupids, + hostids, + applicationids, + recent, + }; + + if (severities) { + params.severities = severities; + } + + if (acknowledged !== undefined) { + params.acknowledged = acknowledged; + } + + if (limit) { + params.limit = limit; + } + + if (timeFrom || timeTo) { + params.time_from = timeFrom; + params.time_till = timeTo; + } + + return this.request('problem.get', params).then(utils.mustArray); + } + + getTriggersByIds(triggerids: string[]) { + const params: any = { + output: 'extend', + triggerids: triggerids, + expandDescription: true, + expandData: true, + expandComment: true, + monitored: true, + skipDependent: true, + selectGroups: ['name'], + selectHosts: ['name', 'host', 'maintenance_status', 'proxy_hostid'], + selectItems: ['name', 'key_', 'lastvalue'], + // selectLastEvent: 'extend', + // selectTags: 'extend', + preservekeys: '1', + }; + + return this.request('trigger.get', params).then(utils.mustArray); + } + + getTriggers(groupids, hostids, applicationids, options) { + const {showTriggers, maintenance, timeFrom, timeTo} = options; + + const params: any = { output: 'extend', groupids: groupids, hostids: hostids, @@ -369,8 +511,10 @@ export class ZabbixAPIConnector { selectTags: 'extend' }; - if (showTriggers) { - params.filter.value = showTriggers; + if (showTriggers === ShowProblemTypes.Problems) { + params.filter.value = 1; + } else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) { + params.filter.value = [0, 1]; } if (maintenance) { @@ -386,7 +530,7 @@ export class ZabbixAPIConnector { } getEvents(objectids, timeFrom, timeTo, showEvents, limit) { - var params = { + const params: any = { output: 'extend', time_from: timeFrom, time_till: timeTo, @@ -402,27 +546,47 @@ export class ZabbixAPIConnector { params.sortorder = 'DESC'; } - return this.request('event.get', params); + return this.request('event.get', params).then(utils.mustArray); } - getAcknowledges(eventids) { - var params = { + getEventsHistory(groupids, hostids, applicationids, options) { + const { timeFrom, timeTo, severities, limit, value } = options; + + const params: any = { output: 'extend', - eventids: eventids, - preservekeys: true, + time_from: timeFrom, + time_till: timeTo, + value: '1', + source: '0', + object: '0', + evaltype: '0', + sortfield: ['eventid'], + sortorder: 'ASC', select_acknowledges: 'extend', - sortfield: 'clock', - sortorder: 'DESC' + selectTags: 'extend', + selectSuppressionData: ['maintenanceid', 'suppress_until'], + groupids, + hostids, + applicationids, }; - return this.request('event.get', params) - .then(events => { - return _.filter(events, (event) => event.acknowledges.length); - }); + if (limit) { + params.limit = limit; + } + + if (severities) { + params.severities = severities; + } + + if (value) { + params.value = value; + } + + return this.request('event.get', params).then(utils.mustArray); } getExtendedEventData(eventids) { - var params = { + const params = { output: 'extend', eventids: eventids, preservekeys: true, @@ -450,8 +614,24 @@ export class ZabbixAPIConnector { return this.request('alert.get', params); } + getAcknowledges(eventids) { + const params = { + output: 'extend', + eventids: eventids, + preservekeys: true, + select_acknowledges: 'extend', + sortfield: 'clock', + sortorder: 'DESC' + }; + + return this.request('event.get', params) + .then(events => { + return _.filter(events, (event) => event.acknowledges.length); + }); + } + getAlerts(itemids, timeFrom, timeTo) { - var params = { + const params: any = { output: 'extend', itemids: itemids, expandDescription: true, @@ -475,8 +655,8 @@ export class ZabbixAPIConnector { } getHostAlerts(hostids, applicationids, options) { - let {minSeverity, acknowledged, count, timeFrom, timeTo} = options; - let params = { + const {minSeverity, acknowledged, count, timeFrom, timeTo} = options; + const params: any = { output: 'extend', hostids: hostids, min_severity: minSeverity, @@ -517,7 +697,7 @@ export class ZabbixAPIConnector { } getProxies() { - var params = { + const params = { output: ['proxyid', 'host'], }; @@ -535,13 +715,17 @@ function filterTriggersByAcknowledge(triggers, acknowledged) { } } -// function isNotAuthorized(message) { -// return ( -// message === "Session terminated, re-login, please." || -// message === "Not authorised." || -// message === "Not authorized." -// ); -// } +function isNotAuthorized(message) { + return ( + message === "Session terminated, re-login, please." || + message === "Not authorised." || + message === "Not authorized." + ); +} + +function isNotInitialized(message) { + return message === "Not initialized"; +} function getSLAInterval(intervalMs) { // Too many intervals may cause significant load on the database, so decrease number of resulting points @@ -550,19 +734,18 @@ function getSLAInterval(intervalMs) { return Math.max(interval, MIN_SLA_INTERVAL); } -function buildSLAIntervals(timeRange, intervalMs) { +function buildSLAIntervals(timeRange, interval) { let [timeFrom, timeTo] = timeRange; - const slaInterval = getSLAInterval(intervalMs); const intervals = []; // Align time range with calculated interval - timeFrom = Math.floor(timeFrom / slaInterval) * slaInterval; - timeTo = Math.ceil(timeTo / slaInterval) * slaInterval; + timeFrom = Math.floor(timeFrom / interval) * interval; + timeTo = Math.ceil(timeTo / interval) * interval; - for (let i = timeFrom; i <= timeTo - slaInterval; i += slaInterval) { + for (let i = timeFrom; i <= timeTo - interval; i += interval) { intervals.push({ from : i, - to : (i + slaInterval) + to : (i + interval) }); } diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.js b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts similarity index 63% rename from src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.js rename to src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts index 97b6c7b..d23880b 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.js +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPICore.ts @@ -1,20 +1,16 @@ /** * General Zabbix API methods */ +import { getBackendSrv } from '@grafana/runtime'; +import { JSONRPCRequest, ZabbixRequestResponse, JSONRPCError, APILoginResponse, GFHTTPRequest, GFRequestOptions } from './types'; export class ZabbixAPICore { - - /** @ngInject */ - constructor(backendSrv) { - this.backendSrv = backendSrv; - } - /** * Request data from Zabbix API * @return {object} response.result */ - request(api_url, method, params, options, auth) { - let requestData = { + request(api_url: string, method: string, params: any, options: GFRequestOptions, auth?: string) { + const requestData: JSONRPCRequest = { jsonrpc: '2.0', method: method, params: params, @@ -23,13 +19,13 @@ export class ZabbixAPICore { if (auth === "") { // Reject immediately if not authenticated - return Promise.reject(new ZabbixAPIError({data: "Not authorised."})); + return Promise.reject(new ZabbixAPIError({data: "Not initialized"})); } else if (auth) { // Set auth parameter only if it needed requestData.auth = auth; } - let requestOptions = { + const requestOptions: GFHTTPRequest = { method: 'POST', url: api_url, data: requestData, @@ -50,18 +46,18 @@ export class ZabbixAPICore { } datasourceRequest(requestOptions) { - return this.backendSrv.datasourceRequest(requestOptions) - .then((response) => { - if (!response.data) { + return getBackendSrv().datasourceRequest(requestOptions) + .then((response: ZabbixRequestResponse) => { + if (!response?.data) { return Promise.reject(new ZabbixAPIError({data: "General Error, no data"})); - } else if (response.data.error) { + } else if (response?.data.error) { // Handle Zabbix API errors return Promise.reject(new ZabbixAPIError(response.data.error)); } // Success - return response.data.result; + return response?.data.result; }); } @@ -69,8 +65,8 @@ export class ZabbixAPICore { * Get authentication token. * @return {string} auth token */ - login(api_url, username, password, options) { - let params = { + login(api_url: string, username: string, password: string, options: GFRequestOptions): Promise { + const params = { user: username, password: password }; @@ -81,14 +77,22 @@ export class ZabbixAPICore { * Get Zabbix API version * Matches the version of Zabbix starting from Zabbix 2.0.4 */ - getVersion(api_url, options) { - return this.request(api_url, 'apiinfo.version', [], options); + getVersion(api_url: string, options: GFRequestOptions): Promise { + return this.request(api_url, 'apiinfo.version', [], options).catch(err => { + console.error(err); + return undefined; + }); } } // Define zabbix API exception type export class ZabbixAPIError { - constructor(error) { + code: number; + name: string; + data: string; + message: string; + + constructor(error: JSONRPCError) { this.code = error.code || null; this.name = error.message || ""; this.data = error.data || ""; diff --git a/src/datasource-zabbix/zabbix/proxy/cachingProxy.js b/src/datasource-zabbix/zabbix/proxy/cachingProxy.ts similarity index 73% rename from src/datasource-zabbix/zabbix/proxy/cachingProxy.js rename to src/datasource-zabbix/zabbix/proxy/cachingProxy.ts index ec36b1f..5d11628 100644 --- a/src/datasource-zabbix/zabbix/proxy/cachingProxy.js +++ b/src/datasource-zabbix/zabbix/proxy/cachingProxy.ts @@ -4,6 +4,10 @@ */ export class CachingProxy { + cacheEnabled: boolean; + ttl: number; + cache: any; + promises: any; constructor(cacheOptions) { this.cacheEnabled = cacheOptions.enabled; @@ -33,13 +37,13 @@ export class CachingProxy { } proxyfyWithCache(func, funcName, funcScope) { - let proxyfied = this.proxyfy(func, funcName, funcScope); + const proxyfied = this.proxyfy(func, funcName, funcScope); return this.cacheRequest(proxyfied, funcName, funcScope); } _isExpired(cacheObject) { if (cacheObject) { - let object_age = Date.now() - cacheObject.timestamp; + const object_age = Date.now() - cacheObject.timestamp; return !(cacheObject.timestamp && object_age < this.ttl); } else { return true; @@ -52,8 +56,9 @@ export class CachingProxy { * with same params when waiting for result. */ function callOnce(func, promiseKeeper, funcScope) { + // tslint:disable-next-line: only-arrow-functions return function() { - var hash = getRequestHash(arguments); + const hash = getRequestHash(arguments); if (!promiseKeeper[hash]) { promiseKeeper[hash] = Promise.resolve( func.apply(funcScope, arguments) @@ -68,22 +73,25 @@ function callOnce(func, promiseKeeper, funcScope) { } function cacheRequest(func, funcName, funcScope, self) { + // tslint:disable-next-line: only-arrow-functions return function() { if (!self.cache[funcName]) { self.cache[funcName] = {}; } - let cacheObject = self.cache[funcName]; - let hash = getRequestHash(arguments); + const cacheObject = self.cache[funcName]; + const hash = getRequestHash(arguments); if (self.cacheEnabled && !self._isExpired(cacheObject[hash])) { return Promise.resolve(cacheObject[hash].value); } else { return func.apply(funcScope, arguments) .then(result => { - cacheObject[hash] = { - value: result, - timestamp: Date.now() - }; + if (result !== undefined) { + cacheObject[hash] = { + value: result, + timestamp: Date.now() + }; + } return result; }); } @@ -92,17 +100,17 @@ function cacheRequest(func, funcName, funcScope, self) { function getRequestHash(args) { const argsJson = JSON.stringify(args); - return argsJson.getHash(); + return getHash(argsJson); } -String.prototype.getHash = function() { - var hash = 0, i, chr, len; - if (this.length !== 0) { - for (i = 0, len = this.length; i < len; i++) { - chr = this.charCodeAt(i); +function getHash(str: string): number { + let hash = 0, i, chr, len; + if (str.length !== 0) { + for (i = 0, len = str.length; i < len; i++) { + chr = str.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } } return hash; -}; +} diff --git a/src/datasource-zabbix/zabbix/types.ts b/src/datasource-zabbix/zabbix/types.ts new file mode 100644 index 0000000..dfc53d9 --- /dev/null +++ b/src/datasource-zabbix/zabbix/types.ts @@ -0,0 +1,23 @@ +export interface ZabbixConnector { + getHistory: (items, timeFrom, timeTill) => Promise; + getTrend: (items, timeFrom, timeTill) => Promise; + getItemsByIDs: (itemids) => Promise; + getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise; + getAlerts: (itemids, timeFrom?, timeTo?) => Promise; + getHostAlerts: (hostids, applicationids, options?) => Promise; + getAcknowledges: (eventids) => Promise; + getITService: (serviceids?) => Promise; + acknowledgeEvent: (eventid, message) => Promise; + getProxies: () => Promise; + getEventAlerts: (eventids) => Promise; + getExtendedEventData: (eventids) => Promise; + getMacros: (hostids: any[]) => Promise; + getVersion: () => Promise; + login: () => Promise; + + getGroups: (groupFilter?) => any; + getHosts: (groupFilter?, hostFilter?) => any; + getApps: (groupFilter?, hostFilter?, appFilter?) => any; + getItems: (groupFilter?, hostFilter?, appFilter?, itemFilter?, options?) => any; + getSLA: (itservices, timeRange, target, options?) => any; +} diff --git a/src/datasource-zabbix/zabbix/zabbix.test.js b/src/datasource-zabbix/zabbix/zabbix.test.js index 44b27c1..45c94b0 100644 --- a/src/datasource-zabbix/zabbix/zabbix.test.js +++ b/src/datasource-zabbix/zabbix/zabbix.test.js @@ -1,6 +1,11 @@ -import mocks from '../../test-setup/mocks'; import { Zabbix } from './zabbix'; +jest.mock('@grafana/runtime', () => ({ + getBackendSrv: () => ({ + datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}), + }), +}), {virtual: true}); + describe('Zabbix', () => { let ctx = {}; let zabbix; @@ -8,14 +13,13 @@ describe('Zabbix', () => { url: 'http://localhost', username: 'zabbix', password: 'zabbix', - zabbixVersion: 4, }; beforeEach(() => { ctx.options = options; - ctx.backendSrv = mocks.backendSrvMock; - ctx.datasourceSrv = mocks.datasourceSrvMock; - zabbix = new Zabbix(ctx.options, ctx.backendSrvMock, ctx.datasourceSrvMock); + // ctx.backendSrv = mocks.backendSrvMock; + // ctx.datasourceSrv = mocks.datasourceSrvMock; + zabbix = new Zabbix(ctx.options); }); describe('When querying proxies', () => { diff --git a/src/datasource-zabbix/zabbix/zabbix.js b/src/datasource-zabbix/zabbix/zabbix.ts similarity index 60% rename from src/datasource-zabbix/zabbix/zabbix.js rename to src/datasource-zabbix/zabbix/zabbix.ts index 677fb62..4847e3e 100644 --- a/src/datasource-zabbix/zabbix/zabbix.js +++ b/src/datasource-zabbix/zabbix/zabbix.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import moment from 'moment'; import * as utils from '../utils'; import responseHandler from '../responseHandler'; import { CachingProxy } from './proxy/cachingProxy'; @@ -7,11 +8,19 @@ import { DBConnector } from './connectors/dbConnector'; import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector'; import { SQLConnector } from './connectors/sql/sqlConnector'; import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector'; +import { ZabbixConnector } from './types'; +import { joinTriggersWithProblems, joinTriggersWithEvents } from '../problemsHandler'; +import { ProblemDTO } from '../types'; + +interface AppsResponse extends Array { + appFilterEmpty?: boolean; + hostids?: any[]; +} const REQUESTS_TO_PROXYFY = [ 'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies', - 'getEventAlerts', 'getExtendedEventData' + 'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds' ]; const REQUESTS_TO_CACHE = [ @@ -24,40 +33,63 @@ const REQUESTS_TO_BIND = [ 'getExtendedEventData' ]; -export class Zabbix { - constructor(options, datasourceSrv, backendSrv, datasourceId) { - let { +export class Zabbix implements ZabbixConnector { + enableDirectDBConnection: boolean; + cachingProxy: CachingProxy; + zabbixAPI: ZabbixAPIConnector; + getHistoryDB: any; + dbConnector: any; + getTrendsDB: any; + + getHistory: (items, timeFrom, timeTill) => Promise; + getTrend: (items, timeFrom, timeTill) => Promise; + getItemsByIDs: (itemids) => Promise; + getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise; + getAlerts: (itemids, timeFrom?, timeTo?) => Promise; + getHostAlerts: (hostids, applicationids, options?) => Promise; + getAcknowledges: (eventids) => Promise; + getITService: (serviceids?) => Promise; + acknowledgeEvent: (eventid, message) => Promise; + getProxies: () => Promise; + getEventAlerts: (eventids) => Promise; + getExtendedEventData: (eventids) => Promise; + getMacros: (hostids: any[]) => Promise; + getVersion: () => Promise; + login: () => Promise; + + constructor(options) { + const { url, username, password, basicAuth, withCredentials, - zabbixVersion, cacheTTL, enableDirectDBConnection, dbConnectionDatasourceId, dbConnectionDatasourceName, dbConnectionRetentionPolicy, + datasourceId, } = options; this.enableDirectDBConnection = enableDirectDBConnection; // Initialize caching proxy for requests - let cacheOptions = { + const cacheOptions = { enabled: true, ttl: cacheTTL }; this.cachingProxy = new CachingProxy(cacheOptions); - this.zabbixAPI = new ZabbixAPIConnector(url, username, password, zabbixVersion, basicAuth, withCredentials, backendSrv, datasourceId); + this.zabbixAPI = new ZabbixAPIConnector(url, username, password, basicAuth, withCredentials, datasourceId); this.proxyfyRequests(); this.cacheRequests(); this.bindRequests(); if (enableDirectDBConnection) { - const connectorOptions = { dbConnectionRetentionPolicy }; - this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv, connectorOptions) + const connectorOptions: any = { dbConnectionRetentionPolicy }; + this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, connectorOptions) .then(() => { this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector); this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector); @@ -65,34 +97,34 @@ export class Zabbix { } } - initDBConnector(datasourceId, datasourceName, datasourceSrv, options) { - return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv) + initDBConnector(datasourceId, datasourceName, options) { + return DBConnector.loadDatasource(datasourceId, datasourceName) .then(ds => { - let connectorOptions = { datasourceId, datasourceName }; + const connectorOptions: any = { datasourceId, datasourceName }; if (ds.type === 'influxdb') { connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy; - this.dbConnector = new InfluxDBConnector(connectorOptions, datasourceSrv); + this.dbConnector = new InfluxDBConnector(connectorOptions); } else { - this.dbConnector = new SQLConnector(connectorOptions, datasourceSrv); + this.dbConnector = new SQLConnector(connectorOptions); } return this.dbConnector; }); } proxyfyRequests() { - for (let request of REQUESTS_TO_PROXYFY) { + for (const request of REQUESTS_TO_PROXYFY) { this.zabbixAPI[request] = this.cachingProxy.proxyfy(this.zabbixAPI[request], request, this.zabbixAPI); } } cacheRequests() { - for (let request of REQUESTS_TO_CACHE) { + for (const request of REQUESTS_TO_CACHE) { this.zabbixAPI[request] = this.cachingProxy.cacheRequest(this.zabbixAPI[request], request, this.zabbixAPI); } } bindRequests() { - for (let request of REQUESTS_TO_BIND) { + for (const request of REQUESTS_TO_BIND) { this[request] = this.zabbixAPI[request].bind(this.zabbixAPI); } } @@ -101,14 +133,14 @@ export class Zabbix { * Perform test query for Zabbix API and external history DB. * @return {object} test result object: * ``` - { - zabbixVersion, - dbConnectorStatus: { - dsType, - dsName - } - } - ``` + * { + * zabbixVersion, + * dbConnectorStatus: { + * dsType, + * dsName + * } + * } + * ``` */ // testDataSource() { // let zabbixVersion; @@ -143,19 +175,20 @@ export class Zabbix { // } getItemsFromTarget(target, options) { - let parts = ['group', 'host', 'application', 'item']; - let filters = _.map(parts, p => target[p].filter); + const parts = ['group', 'host', 'application', 'item']; + const filters = _.map(parts, p => target[p].filter); return this.getItems(...filters, options); } getHostsFromTarget(target) { - let parts = ['group', 'host', 'application']; - let filters = _.map(parts, p => target[p].filter); + const parts = ['group', 'host', 'application']; + const filters = _.map(parts, p => target[p].filter); return Promise.all([ this.getHosts(...filters), this.getApps(...filters), - ]).then((results) => { - let [hosts, apps] = results; + ]).then(results => { + const hosts = results[0]; + let apps: AppsResponse = results[1]; if (apps.appFilterEmpty) { apps = []; } @@ -178,12 +211,12 @@ export class Zabbix { getAllHosts(groupFilter) { return this.getGroups(groupFilter) .then(groups => { - let groupids = _.map(groups, 'groupid'); + const groupids = _.map(groups, 'groupid'); return this.zabbixAPI.getHosts(groupids); }); } - getHosts(groupFilter, hostFilter) { + getHosts(groupFilter?, hostFilter?) { return this.getAllHosts(groupFilter) .then(hosts => findByFilter(hosts, hostFilter)); } @@ -194,34 +227,34 @@ export class Zabbix { getAllApps(groupFilter, hostFilter) { return this.getHosts(groupFilter, hostFilter) .then(hosts => { - let hostids = _.map(hosts, 'hostid'); + const hostids = _.map(hosts, 'hostid'); return this.zabbixAPI.getApps(hostids); }); } - getApps(groupFilter, hostFilter, appFilter) { + getApps(groupFilter?, hostFilter?, appFilter?): Promise { return this.getHosts(groupFilter, hostFilter) .then(hosts => { - let hostids = _.map(hosts, 'hostid'); + const hostids = _.map(hosts, 'hostid'); if (appFilter) { return this.zabbixAPI.getApps(hostids) .then(apps => filterByQuery(apps, appFilter)); } else { - return { - appFilterEmpty: true, - hostids: hostids - }; + const appsResponse: AppsResponse = hostids; + appsResponse.hostids = hostids; + appsResponse.appFilterEmpty = true; + return Promise.resolve(appsResponse); } }); } - getAllItems(groupFilter, hostFilter, appFilter, options = {}) { + getAllItems(groupFilter, hostFilter, appFilter, options: any = {}) { return this.getApps(groupFilter, hostFilter, appFilter) .then(apps => { if (apps.appFilterEmpty) { return this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype); } else { - let appids = _.map(apps, 'applicationid'); + const appids = _.map(apps, 'applicationid'); return this.zabbixAPI.getItems(undefined, appids, options.itemtype); } }) @@ -235,34 +268,54 @@ export class Zabbix { .then(this.expandUserMacro.bind(this)); } - expandUserMacro(items) { - let hostids = getHostIds(items); + expandUserMacro(items, isTriggerItem) { + const hostids = getHostIds(items); return this.getMacros(hostids) .then(macros => { _.forEach(items, item => { - if (utils.containsMacro(item.name)) { - item.name = utils.replaceMacro(item, macros); + if (utils.containsMacro(isTriggerItem ? item.url : item.name)) { + if (isTriggerItem) { + item.url = utils.replaceMacro(item, macros, isTriggerItem); + } else { + item.name = utils.replaceMacro(item, macros); + } } }); return items; }); } - getItems(groupFilter, hostFilter, appFilter, itemFilter, options = {}) { + getItems(groupFilter?, hostFilter?, appFilter?, itemFilter?, options = {}) { return this.getAllItems(groupFilter, hostFilter, appFilter, options) .then(items => filterByQuery(items, itemFilter)); } + getItemValues(groupFilter?, hostFilter?, appFilter?, itemFilter?, options: any = {}) { + return this.getItems(groupFilter, hostFilter, appFilter, itemFilter, options).then(items => { + let timeRange = [moment().subtract(2, 'h').unix(), moment().unix()]; + if (options.range) { + timeRange = [options.range.from.unix(), options.range.to.unix()]; + } + const [timeFrom, timeTo] = timeRange; + + return this.zabbixAPI.getHistory(items, timeFrom, timeTo).then(history => { + if (history) { + const values = _.uniq(history.map(v => v.value)); + return values.map(value => ({ name: value })); + } else { + return []; + } + }); + }); + } + getITServices(itServiceFilter) { return this.zabbixAPI.getITService() .then(itServices => findByFilter(itServices, itServiceFilter)); } - /** - * Build query - convert target filters to array of Zabbix items - */ - getTriggers(groupFilter, hostFilter, appFilter, options, proxyFilter) { - let promises = [ + getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?) { + const promises = [ this.getGroups(groupFilter), this.getHosts(groupFilter, hostFilter), this.getApps(groupFilter, hostFilter, appFilter) @@ -270,8 +323,8 @@ export class Zabbix { return Promise.all(promises) .then(results => { - let [filteredGroups, filteredHosts, filteredApps] = results; - let query = {}; + const [filteredGroups, filteredHosts, filteredApps] = results; + const query: any = {}; if (appFilter) { query.applicationids = _.flatten(_.map(filteredApps, 'applicationid')); @@ -285,8 +338,53 @@ export class Zabbix { return query; }) - .then(query => this.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options)) - .then(triggers => this.filterTriggersByProxy(triggers, proxyFilter)); + .then(query => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options)) + .then(problems => { + const triggerids = problems?.map(problem => problem.objectid); + return Promise.all([ + Promise.resolve(problems), + this.zabbixAPI.getTriggersByIds(triggerids) + ]); + }) + .then(([problems, triggers]) => joinTriggersWithProblems(problems, triggers)) + .then(triggers => this.filterTriggersByProxy(triggers, proxyFilter)) + .then(triggers => this.expandUserMacro.bind(this)(triggers, true)); + } + + getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter?, options?): Promise { + const { valueFromEvent } = options; + + const promises = [ + this.getGroups(groupFilter), + this.getHosts(groupFilter, hostFilter), + this.getApps(groupFilter, hostFilter, appFilter) + ]; + + return Promise.all(promises) + .then(results => { + const [filteredGroups, filteredHosts, filteredApps] = results; + const query: any = {}; + + if (appFilter) { + query.applicationids = _.flatten(_.map(filteredApps, 'applicationid')); + } + if (hostFilter) { + query.hostids = _.map(filteredHosts, 'hostid'); + } + if (groupFilter) { + query.groupids = _.map(filteredGroups, 'groupid'); + } + + return query; + }) + .then(query => this.zabbixAPI.getEventsHistory(query.groupids, query.hostids, query.applicationids, options)) + .then(problems => { + const triggerids = problems?.map(problem => problem.objectid); + return Promise.all([Promise.resolve(problems), this.zabbixAPI.getTriggersByIds(triggerids)]); + }) + .then(([problems, triggers]) => joinTriggersWithEvents(problems, triggers, { valueFromEvent })) + .then(triggers => this.filterTriggersByProxy(triggers, proxyFilter)) + .then(triggers => this.expandUserMacro.bind(this)(triggers, true)); } filterTriggersByProxy(triggers, proxyFilter) { @@ -295,14 +393,13 @@ export class Zabbix { if (proxyFilter && proxyFilter !== '/.*/' && triggers) { const proxy_ids = proxies.map(proxy => proxy.proxyid); triggers = triggers.filter(trigger => { - let filtered = false; - for(let i = 0; i < trigger.hosts.length; i++) { + for (let i = 0; i < trigger.hosts.length; i++) { const host = trigger.hosts[i]; if (proxy_ids.includes(host.proxy_hostid)) { - filtered = true; + return true; } } - return filtered; + return false; }); } return triggers; @@ -318,7 +415,7 @@ export class Zabbix { } getHistoryTS(items, timeRange, options) { - let [timeFrom, timeTo] = timeRange; + const [timeFrom, timeTo] = timeRange; if (this.enableDirectDBConnection) { return this.getHistoryDB(items, timeFrom, timeTo, options) .then(history => this.dbConnector.handleGrafanaTSResponse(history, items)); @@ -329,12 +426,12 @@ export class Zabbix { } getTrends(items, timeRange, options) { - let [timeFrom, timeTo] = timeRange; + const [timeFrom, timeTo] = timeRange; if (this.enableDirectDBConnection) { return this.getTrendsDB(items, timeFrom, timeTo, options) .then(history => this.dbConnector.handleGrafanaTSResponse(history, items)); } else { - let valueType = options.consolidateBy || options.valueType; + const valueType = options.consolidateBy || options.valueType; return this.zabbixAPI.getTrend(items, timeFrom, timeTo) .then(history => responseHandler.handleTrends(history, items, valueType)) .then(responseHandler.sortTimeseries); // Sort trend data, issue #202 @@ -342,7 +439,7 @@ export class Zabbix { } getHistoryText(items, timeRange, target) { - let [timeFrom, timeTo] = timeRange; + const [timeFrom, timeTo] = timeRange; if (items.length) { return this.zabbixAPI.getHistory(items, timeFrom, timeTo) .then(history => { @@ -358,15 +455,11 @@ export class Zabbix { } getSLA(itservices, timeRange, target, options) { - let itServices = itservices; - if (options.isOldVersion) { - itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid}); - } - let itServiceIds = _.map(itServices, 'serviceid'); + const itServiceIds = _.map(itservices, 'serviceid'); return this.zabbixAPI.getSLA(itServiceIds, timeRange, options) .then(slaResponse => { return _.map(itServiceIds, serviceid => { - let itservice = _.find(itServices, {'serviceid': serviceid}); + const itservice = _.find(itservices, {'serviceid': serviceid}); return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse); }); }); @@ -382,7 +475,7 @@ export class Zabbix { * @return array with finded element or empty array */ function findByName(list, name) { - var finded = _.find(list, {'name': name}); + const finded = _.find(list, {'name': name}); if (finded) { return [finded]; } else { @@ -399,7 +492,7 @@ function findByName(list, name) { * @return {[type]} array with finded element or empty array */ function filterByName(list, name) { - var finded = _.filter(list, {'name': name}); + const finded = _.filter(list, {'name': name}); if (finded) { return finded; } else { @@ -408,8 +501,8 @@ function filterByName(list, name) { } function filterByRegex(list, regex) { - var filterPattern = utils.buildRegex(regex); - return _.filter(list, function (zbx_obj) { + const filterPattern = utils.buildRegex(regex); + return _.filter(list, (zbx_obj) => { return filterPattern.test(zbx_obj.name); }); } @@ -431,7 +524,7 @@ function filterByQuery(list, filter) { } function getHostIds(items) { - let hostIds = _.map(items, item => { + const hostIds = _.map(items, item => { return _.map(item.hosts, 'hostid'); }); return _.uniq(_.flatten(hostIds)); diff --git a/src/datasource-zabbix/zabbixAlerting.service.js b/src/datasource-zabbix/zabbixAlerting.service.js index 438c5c2..413d6d2 100644 --- a/src/datasource-zabbix/zabbixAlerting.service.js +++ b/src/datasource-zabbix/zabbixAlerting.service.js @@ -16,8 +16,11 @@ export class ZabbixAlertingService { } setPanelAlertState(panelId, alertState) { - let panelIndex; + if (!alertState) { + return; + } + let panelIndex; let panelContainers = _.filter($('.panel-container'), elem => { return elem.clientHeight && elem.clientWidth; }); @@ -32,8 +35,7 @@ export class ZabbixAlertingService { }); } - // Don't apply alert styles to .panel-container--absolute (it rewrites position from absolute to relative) - if (panelIndex >= 0 && !panelContainers[panelIndex].className.includes('panel-container--absolute')) { + if (panelIndex >= 0) { let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting"; $(panelContainers[panelIndex]).removeClass(alertClass); diff --git a/src/img/icn-zabbix-app.svg b/src/img/icn-zabbix-app.svg new file mode 100644 index 0000000..6878e22 --- /dev/null +++ b/src/img/icn-zabbix-app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/zabbix_app_logo.svg b/src/img/zabbix_app_logo.svg deleted file mode 100755 index 237247d..0000000 --- a/src/img/zabbix_app_logo.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/src/module.js b/src/module.js index 58fed40..0854e5c 100644 --- a/src/module.js +++ b/src/module.js @@ -1,7 +1,7 @@ import './sass/grafana-zabbix.dark.scss'; import './sass/grafana-zabbix.light.scss'; -import {ZabbixAppConfigCtrl} from './components/config'; +import {ZabbixAppConfigCtrl} from './app_config_ctrl/config'; import {loadPluginCss} from 'grafana/app/plugins/sdk'; loadPluginCss({ diff --git a/src/panel-triggers/components/AckModal.tsx b/src/panel-triggers/components/AckModal.tsx new file mode 100644 index 0000000..0d3e16b --- /dev/null +++ b/src/panel-triggers/components/AckModal.tsx @@ -0,0 +1,277 @@ +import React, { PureComponent } from 'react'; +import { cx, css } from 'emotion'; +import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants'; +import { Button, VerticalGroup, Spinner, Modal, Input, Forms, stylesFactory, withTheme, Themeable } from '@grafana/ui'; +import { FAIcon } from '../../components'; + +import * as grafanaUi from '@grafana/ui'; +import { GrafanaTheme } from '@grafana/data'; +const Checkbox: any = Forms?.Checkbox || (grafanaUi as any).Checkbox; +const RadioButtonGroup: any = Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup; + +const KEYBOARD_ENTER_KEY = 13; +const KEYBOARD_ESCAPE_KEY = 27; + +interface Props extends Themeable { + canAck?: boolean; + canClose?: boolean; + severity?: number; + withBackdrop?: boolean; + onSubmit: (data?: AckProblemData) => Promise | any; + onDismiss?: () => void; +} + +interface State { + value: string; + error: boolean; + errorMessage: string; + ackError: string; + acknowledge: boolean; + closeProblem: boolean; + changeSeverity: boolean; + selectedSeverity: number; + loading: boolean; +} + +export interface AckProblemData { + message: string; + closeProblem?: boolean; + action?: number; + severity?: number; +} + +const severityOptions = [ + {value: 0, label: 'Not classified'}, + {value: 1, label: 'Information'}, + {value: 2, label: 'Warning'}, + {value: 3, label: 'Average'}, + {value: 4, label: 'High'}, + {value: 5, label: 'Disaster'} +]; + +export class AckModalUnthemed extends PureComponent { + static defaultProps: Partial = { + withBackdrop: true, + }; + + constructor(props) { + super(props); + this.state = { + value: '', + error: false, + errorMessage: '', + ackError: '', + acknowledge: false, + closeProblem: false, + changeSeverity: false, + selectedSeverity: props.severity || 0, + loading: false, + }; + } + + handleChange = (event: React.ChangeEvent) => { + this.setState({ value: event.target.value, error: false }); + } + + handleKeyUp = (event: React.KeyboardEvent) => { + if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') { + this.submit(); + } else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') { + this.dismiss(); + } + } + + handleBackdropClick = () => { + this.dismiss(); + } + + onAcknowledgeToggle = () => { + this.setState({ acknowledge: !this.state.acknowledge, error: false }); + } + + onChangeSeverityToggle = () => { + this.setState({ changeSeverity: !this.state.changeSeverity, error: false }); + } + + onCloseProblemToggle = () => { + this.setState({ closeProblem: !this.state.closeProblem, error: false }); + } + + onChangeSelectedSeverity = v => { + this.setState({ selectedSeverity: v }); + }; + + dismiss = () => { + this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false }); + this.props.onDismiss(); + } + + submit = () => { + const { acknowledge, changeSeverity, closeProblem } = this.state; + + const actionSelected = acknowledge || changeSeverity || closeProblem; + if (!this.state.value && !actionSelected) { + return this.setState({ + error: true, + errorMessage: 'Enter message text or select an action' + }); + } + + this.setState({ ackError: '', loading: true }); + + const ackData: AckProblemData = { + message: this.state.value, + }; + + let action = ZBX_ACK_ACTION_ADD_MESSAGE; + if (this.state.acknowledge) { + action += ZBX_ACK_ACTION_ACK; + } + if (this.state.changeSeverity) { + action += ZBX_ACK_ACTION_CHANGE_SEVERITY; + ackData.severity = this.state.selectedSeverity; + } + if (this.state.closeProblem) { + action += ZBX_ACK_ACTION_CLOSE; + } + ackData.action = action; + + this.props.onSubmit(ackData).then(() => { + this.dismiss(); + }).catch(err => { + this.setState({ + ackError: err.message || err.data, + loading: false, + }); + }); + } + + renderActions() { + const { canClose } = this.props; + + const actions = [ + , + , + this.state.changeSeverity && + , + canClose && + , + ]; + + // doesn't handle empty elements properly, so don't return it + return actions.filter(e => e); + } + + render() { + const { theme } = this.props; + + const styles = getStyles(theme); + const modalClass = cx(styles.modal); + const modalTitleClass = cx(styles.modalHeaderTitle); + const inputGroupClass = cx('gf-form', styles.inputGroup); + const inputClass = cx(this.state.error && styles.input); + const inputHintClass = cx('gf-form-hint-text', styles.inputHint); + const inputErrorClass = cx('gf-form-hint-text', styles.inputError); + + return ( + + {this.state.loading ? : } + Acknowledge Problem +
+ } + > +
+ +
+ +
+ + {this.renderActions()} + +
+ + {this.state.ackError && +
+ {this.state.ackError} +
+ } + +
+ + +
+ + ); + } +} + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const red = theme.colors.red || (theme as any).palette.red; + return { + modal: css` + width: 500px; + `, + modalHeaderTitle: css` + font-size: ${theme.typography.heading.h3}; + padding-top: ${theme.spacing.sm}; + margin: 0 ${theme.spacing.md}; + display: flex; + `, + inputGroup: css` + margin-bottom: 16px; + `, + input: css` + border-color: ${red}; + border-radius: 2px; + outline-offset: 2px; + box-shadow: 0 0 0 2px ${theme.colors.pageBg || (theme as any).colors.bg1}, 0 0 0px 4px ${red}; + `, + inputHint: css` + display: inherit; + float: right; + color: ${theme.colors.textWeak}; + `, + inputError: css` + float: left; + color: ${red}; + `, + ackError: css` + color: ${red}; + `, + }; +}); + +export const AckModal = withTheme(AckModalUnthemed); diff --git a/src/panel-triggers/components/AlertList/AlertCard.tsx b/src/panel-triggers/components/AlertList/AlertCard.tsx index 87eaf9d..ad738f3 100644 --- a/src/panel-triggers/components/AlertList/AlertCard.tsx +++ b/src/panel-triggers/components/AlertList/AlertCard.tsx @@ -3,29 +3,22 @@ import classNames from 'classnames'; import _ from 'lodash'; import moment from 'moment'; import { isNewProblem, formatLastChange } from '../../utils'; -import { ProblemsPanelOptions, ZBXTrigger, ZBXTag } from '../../types'; -import { AckProblemData, Modal } from '.././Modal'; +import { ProblemsPanelOptions, TriggerSeverity } from '../../types'; +import { AckProblemData, AckModal } from '../AckModal'; import EventTag from '../EventTag'; -import Tooltip from '.././Tooltip/Tooltip'; import AlertAcknowledges from './AlertAcknowledges'; import AlertIcon from './AlertIcon'; +import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types'; +import { ModalController, Tooltip } from '../../../components'; interface AlertCardProps { - problem: ZBXTrigger; + problem: ProblemDTO; panelOptions: ProblemsPanelOptions; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; - onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise | any; + onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise | any; } -interface AlertCardState { - showAckDialog: boolean; -} - -export default class AlertCard extends PureComponent { - constructor(props) { - super(props); - this.state = { showAckDialog: false }; - } +export default class AlertCard extends PureComponent { handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => { if (this.props.onTagClick) { @@ -35,23 +28,7 @@ export default class AlertCard extends PureComponent { const problem = this.props.problem; - return this.props.onProblemAck(problem, data).then(result => { - this.closeAckDialog(); - }).catch(err => { - console.log(err); - this.closeAckDialog(); - }); - } - - showAckDialog = () => { - const problem = this.props.problem; - if (problem.showAckButton) { - this.setState({ showAckDialog: true }); - } - } - - closeAckDialog = () => { - this.setState({ showAckDialog: false }); + return this.props.onProblemAck(problem, data); } render() { @@ -59,9 +36,16 @@ export default class AlertCard extends PureComponent 1; const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground }); const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine }); - const severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority)); - const lastchange = formatLastChange(problem.lastchangeUnix, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat); - const age = moment.unix(problem.lastchangeUnix).fromNow(true); + + const problemSeverity = Number(problem.severity); + let severityDesc: TriggerSeverity; + severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); + if (problem.severity) { + severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); + } + + const lastchange = formatLastChange(problem.timestamp, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat); + const age = moment.unix(problem.timestamp).fromNow(true); let newProblem = false; if (panelOptions.highlightNewerThan) { @@ -72,7 +56,7 @@ export default class AlertCard extends PureComponent )} - {problem.lastEvent && ( - + {problem.eventid && ( + + {({ showModal, hideModal }) => ( + { + showModal(AckModal, { + canClose: problem.manual_close === '1', + severity: problemSeverity, + onSubmit: this.ackProblem, + onDismiss: hideModal, + }); + }} + /> + )} + )}
- ); } } interface AlertHostProps { - problem: ZBXTrigger; + problem: ProblemDTO; panelOptions: ProblemsPanelOptions; } @@ -194,7 +188,7 @@ function AlertHost(props: AlertHostProps) { } interface AlertGroupProps { - problem: ZBXTrigger; + problem: ProblemDTO; panelOptions: ProblemsPanelOptions; } @@ -247,7 +241,7 @@ function AlertSeverity(props) { } interface AlertAcknowledgesButtonProps { - problem: ZBXTrigger; + problem: ProblemDTO; onClick: (event?) => void; } diff --git a/src/panel-triggers/components/AlertList/AlertIcon.tsx b/src/panel-triggers/components/AlertList/AlertIcon.tsx index 2b69d31..936208e 100644 --- a/src/panel-triggers/components/AlertList/AlertIcon.tsx +++ b/src/panel-triggers/components/AlertList/AlertIcon.tsx @@ -1,33 +1,34 @@ -import React, { CSSProperties } from 'react'; -import classNames from 'classnames'; -import { ZBXTrigger } from '../../types'; +import React, { FC } from 'react'; +import { cx, css } from 'emotion'; +import { GFHeartIcon } from '../../../components'; +import { ProblemDTO } from '../../../datasource-zabbix/types'; -interface AlertIconProps { - problem: ZBXTrigger; +interface Props { + problem: ProblemDTO; color: string; blink?: boolean; highlightBackground?: boolean; } -export default function AlertIcon(props: AlertIconProps) { - const { problem, color, blink, highlightBackground } = props; - const priority = Number(problem.priority); - let iconClass = ''; - if (problem.value === '1' && priority >= 2) { - iconClass = 'icon-gf-critical'; - } else { - iconClass = 'icon-gf-online'; - } +export const AlertIcon: FC = ({ problem, color, blink, highlightBackground }) => { + const severity = Number(problem.severity); + const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online'; - const className = classNames('icon-gf', iconClass, { 'zabbix-trigger--blinked': blink }); - const style: CSSProperties = {}; - if (!highlightBackground) { - style.color = color; - } + const iconClass = cx( + 'icon-gf', + blink && 'zabbix-trigger--blinked', + ); + + const wrapperClass = cx( + 'alert-rule-item__icon', + !highlightBackground && css`color: ${color}` + ); return ( -
- +
+
); -} +}; + +export default AlertIcon; diff --git a/src/panel-triggers/components/AlertList/AlertList.tsx b/src/panel-triggers/components/AlertList/AlertList.tsx index a01c155..0f50de5 100644 --- a/src/panel-triggers/components/AlertList/AlertList.tsx +++ b/src/panel-triggers/components/AlertList/AlertList.tsx @@ -1,23 +1,24 @@ import React, { PureComponent, CSSProperties } from 'react'; import classNames from 'classnames'; -import { ProblemsPanelOptions, ZBXTrigger, GFTimeRange, ZBXTag } from '../../types'; -import { AckProblemData } from '.././Modal'; +import { ProblemsPanelOptions, GFTimeRange } from '../../types'; +import { AckProblemData } from '../AckModal'; import AlertCard from './AlertCard'; +import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types'; export interface AlertListProps { - problems: ZBXTrigger[]; + problems: ProblemDTO[]; panelOptions: ProblemsPanelOptions; loading?: boolean; timeRange?: GFTimeRange; pageSize?: number; fontSize?: number; - onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; + onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; } interface AlertListState { page: number; - currentProblems: ZBXTrigger[]; + currentProblems: ProblemDTO[]; } export default class AlertList extends PureComponent { @@ -51,7 +52,7 @@ export default class AlertList extends PureComponent { + handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => { return this.props.onProblemAck(problem, data); } @@ -68,7 +69,7 @@ export default class AlertList extends PureComponent {currentProblems.map(problem => - - - ); -} diff --git a/src/panel-triggers/components/GFHeartIcon.tsx b/src/panel-triggers/components/GFHeartIcon.tsx deleted file mode 100644 index 23c1826..0000000 --- a/src/panel-triggers/components/GFHeartIcon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; - -interface GFHeartIconProps { - status: 'critical' | 'warning' | 'online' | 'ok' | 'problem'; - className?: string; -} - -export default function GFHeartIcon(props: GFHeartIconProps) { - const status = props.status; - const className = classNames("icon-gf", props.className, - { "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'}, - { "icon-gf-online": status === 'online' || status === 'ok' }, - ); - return ( - - ); -} diff --git a/src/panel-triggers/components/Modal.tsx b/src/panel-triggers/components/Modal.tsx deleted file mode 100644 index 4cedb8d..0000000 --- a/src/panel-triggers/components/Modal.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { PureComponent } from 'react'; -import ReactDOM from 'react-dom'; -import classNames from 'classnames'; - -const KEYBOARD_ENTER_KEY = 13; -const KEYBOARD_ESCAPE_KEY = 27; - -interface ModalProps { - isOpen?: boolean; - withBackdrop?: boolean; - onSubmit: (data?: AckProblemData) => Promise | any; - onClose?: () => void; -} - -interface ModalState { - value: string; - error: boolean; - message: string; -} - -export interface AckProblemData { - message: string; - closeProblem?: boolean; - action?: number; -} - -export class Modal extends PureComponent { - modalContainer: HTMLElement; - - constructor(props) { - super(props); - this.state = { - value: '', - error: false, - message: '', - }; - - this.modalContainer = document.body; - } - - handleChange = (event: React.ChangeEvent) => { - this.setState({ value: event.target.value, error: false }); - } - - handleKeyUp = (event: React.KeyboardEvent) => { - if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') { - this.submit(); - } else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') { - this.dismiss(); - } - } - - handleBackdropClick = () => { - this.dismiss(); - } - - dismiss = () => { - this.setState({ value: '', error: false, message: '' }); - this.props.onClose(); - } - - submit = () => { - if (!this.state.value) { - return this.setState({ - error: true, - message: 'Enter message text' - }); - } - this.props.onSubmit({ - message: this.state.value - }).then(() => { - this.dismiss(); - }); - } - - render() { - if (!this.props.isOpen || !this.modalContainer) { - return null; - } - - const inputClass = classNames('gf-form-input', { 'zbx-ack-error': this.state.error }); - - const modalNode = ( -
-
-
-

- - Acknowledge Problem -

- - - - -
-
-
- -
- -
- - -
-
-
-
- ); - - const modalNodeWithBackdrop = [ - modalNode, -
- ]; - - const modal = this.props.withBackdrop ? modalNodeWithBackdrop : modalNode; - return ReactDOM.createPortal(modal, this.modalContainer); - } -} diff --git a/src/panel-triggers/components/Problems/AckCell.tsx b/src/panel-triggers/components/Problems/AckCell.tsx new file mode 100644 index 0000000..8b5d529 --- /dev/null +++ b/src/panel-triggers/components/Problems/AckCell.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { css } from 'emotion'; +import { RTCell } from '../../types'; +import { ProblemDTO } from '../../../datasource-zabbix/types'; +import { FAIcon } from '../../../components'; +import { useTheme, stylesFactory } from '@grafana/ui'; +import { GrafanaTheme } from '@grafana/data'; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + countLabel: css` + font-size: ${theme.typography.size.sm}; + `, + }; +}); + +export const AckCell: React.FC> = (props: RTCell) => { + const problem = props.original; + const theme = useTheme(); + const styles = getStyles(theme); + + return ( +
+ {problem.acknowledges?.length > 0 && + <> + + ({problem.acknowledges?.length}) + + } +
+ ); +}; + +export default AckCell; diff --git a/src/panel-triggers/components/Problems/ProblemDetails.tsx b/src/panel-triggers/components/Problems/ProblemDetails.tsx index 67154b1..4514b3b 100644 --- a/src/panel-triggers/components/Problems/ProblemDetails.tsx +++ b/src/panel-triggers/components/Problems/ProblemDetails.tsx @@ -1,22 +1,23 @@ import React, { PureComponent } from 'react'; import moment from 'moment'; import * as utils from '../../../datasource-zabbix/utils'; -import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag, ZBXAlert } from '../../types'; -import { Modal, AckProblemData } from '../Modal'; +import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types'; +import { ZBXItem, GFTimeRange, RTRow } from '../../types'; +import { AckModal, AckProblemData } from '../AckModal'; import EventTag from '../EventTag'; -import Tooltip from '../Tooltip/Tooltip'; import ProblemStatusBar from './ProblemStatusBar'; import AcknowledgesList from './AcknowledgesList'; import ProblemTimeline from './ProblemTimeline'; -import FAIcon from '../FAIcon'; +import { FAIcon, ExploreButton, AckButton, Tooltip, ModalController } from '../../../components'; -interface ProblemDetailsProps extends RTRow { +interface ProblemDetailsProps extends RTRow { rootWidth: number; timeRange: GFTimeRange; showTimeline?: boolean; - getProblemEvents: (problem: ZBXTrigger) => Promise; - getProblemAlerts: (problem: ZBXTrigger) => Promise; - onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise | any; + panelId?: number; + getProblemEvents: (problem: ProblemDTO) => Promise; + getProblemAlerts: (problem: ProblemDTO) => Promise; + onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise | any; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; } @@ -24,17 +25,15 @@ interface ProblemDetailsState { events: ZBXEvent[]; alerts: ZBXAlert[]; show: boolean; - showAckDialog: boolean; } -export default class ProblemDetails extends PureComponent { +export class ProblemDetails extends PureComponent { constructor(props) { super(props); this.state = { events: [], alerts: [], show: false, - showAckDialog: false, }; } @@ -71,32 +70,20 @@ export default class ProblemDetails extends PureComponent { - const problem = this.props.original as ZBXTrigger; - return this.props.onProblemAck(problem, data).then(result => { - this.closeAckDialog(); - }).catch(err => { - console.log(err); - this.closeAckDialog(); - }); - } - - showAckDialog = () => { - this.setState({ showAckDialog: true }); - } - - closeAckDialog = () => { - this.setState({ showAckDialog: false }); + const problem = this.props.original as ProblemDTO; + return this.props.onProblemAck(problem, data); } render() { - const problem = this.props.original as ZBXTrigger; + const problem = this.props.original as ProblemDTO; const alerts = this.state.alerts; const rootWidth = this.props.rootWidth; const displayClass = this.state.show ? 'show' : ''; const wideLayout = rootWidth > 1200; const compactStatusBar = rootWidth < 800 || problem.acknowledges && wideLayout && rootWidth < 1400; - const age = moment.unix(problem.lastchangeUnix).fromNow(true); + const age = moment.unix(problem.timestamp).fromNow(true); const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0; + const problemSeverity = Number(problem.severity); return (
@@ -109,20 +96,36 @@ export default class ProblemDetails extends PureComponent {problem.items && }
+
+ +
{problem.showAckButton &&
- + + {({ showModal, hideModal }) => ( + { + showModal(AckModal, { + canClose: problem.manual_close === '1', + severity: problemSeverity, + onSubmit: this.ackProblem, + onDismiss: hideModal, + }); + }} + /> + )} +
}
{problem.comments && -
- Description:  - {problem.comments} +
+
+ Description:  + {problem.comments} +
} {problem.tags && problem.tags.length > 0 && @@ -169,10 +172,6 @@ export default class ProblemDetails extends PureComponent} {problem.hosts && }
-
); } @@ -242,33 +241,3 @@ class ProblemHosts extends PureComponent { )); } } - -interface ProblemActionButtonProps { - icon: string; - tooltip?: string; - className?: string; - onClick?: (event?) => void; -} - -class ProblemActionButton extends PureComponent { - handleClick = (event) => { - this.props.onClick(event); - } - - render() { - const { icon, tooltip, className } = this.props; - let button = ( - - ); - if (tooltip) { - button = ( - - {button} - - ); - } - return button; - } -} diff --git a/src/panel-triggers/components/Problems/ProblemStatusBar.tsx b/src/panel-triggers/components/Problems/ProblemStatusBar.tsx index b88fd60..e6a18de 100644 --- a/src/panel-triggers/components/Problems/ProblemStatusBar.tsx +++ b/src/panel-triggers/components/Problems/ProblemStatusBar.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import FAIcon from '../FAIcon'; -import Tooltip from '../Tooltip/Tooltip'; +import FAIcon from '../../../components/FAIcon/FAIcon'; +import Tooltip from '../../../components/Tooltip/Tooltip'; import { ZBXTrigger, ZBXAlert } from '../../types'; export interface ProblemStatusBarProps { diff --git a/src/panel-triggers/components/Problems/ProblemTimeline.tsx b/src/panel-triggers/components/Problems/ProblemTimeline.tsx index b96f126..3270e1a 100644 --- a/src/panel-triggers/components/Problems/ProblemTimeline.tsx +++ b/src/panel-triggers/components/Problems/ProblemTimeline.tsx @@ -371,7 +371,7 @@ class TimelineRegions extends PureComponent { }; return ( - + ); }); @@ -480,7 +480,7 @@ class TimelinePoints extends PureComponent { return ( Promise; - getProblemAlerts: (problem: ZBXTrigger) => Promise; - onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; + panelId?: number; + getProblemEvents: (problem: ProblemDTO) => Promise; + getProblemAlerts: (problem: ProblemDTO) => Promise; + onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void; onColumnResize?: (newResized: RTResized) => void; @@ -47,7 +49,7 @@ export default class ProblemList extends PureComponent { + handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => { return this.props.onProblemAck(problem, data); } @@ -85,19 +87,21 @@ export default class ProblemList extends PureComponent StatusCell(props, options.okEventColor, DEFAULT_PROBLEM_COLOR, highlightNewerThan); + const statusCell = props => StatusCell(props, highlightNewerThan); const statusIconCell = props => StatusIconCell(props, highlightNewerThan); + const hostNameCell = props => ; + const hostTechNameCell = props => ; const columns = [ - { Header: 'Host', accessor: 'host', show: options.hostField }, - { Header: 'Host (Technical Name)', accessor: 'hostTechName', show: options.hostTechNameField }, + { Header: 'Host', id: 'host', show: options.hostField, Cell: hostNameCell }, + { Header: 'Host (Technical Name)', id: 'hostTechName', show: options.hostTechNameField, Cell: hostTechNameCell }, { Header: 'Host Groups', accessor: 'groups', show: options.hostGroups, Cell: GroupCell }, { Header: 'Proxy', accessor: 'proxy', show: options.hostProxy }, { Header: 'Severity', show: options.severityField, className: 'problem-severity', width: 120, accessor: problem => problem.priority, id: 'severity', - Cell: props => SeverityCell(props, options.triggerSeverity, options.markAckEvents, options.ackEventColor), + Cell: props => SeverityCell(props, options.triggerSeverity, options.markAckEvents, options.ackEventColor, options.okEventColor), }, { Header: '', id: 'statusIcon', show: options.statusIcon, className: 'problem-status-icon', width: 50, @@ -106,17 +110,21 @@ export default class ProblemList extends PureComponent + }, { Header: 'Tags', accessor: 'tags', show: options.showTags, className: 'problem-tags', Cell: props => }, { - Header: 'Age', className: 'problem-age', width: 100, show: options.ageField, accessor: 'lastchangeUnix', + Header: 'Age', className: 'problem-age', width: 100, show: options.ageField, accessor: 'timestamp', id: 'age', Cell: AgeCell, }, { - Header: 'Time', className: 'last-change', width: 150, accessor: 'lastchangeUnix', + Header: 'Time', className: 'last-change', width: 150, accessor: 'timestamp', id: 'lastchange', Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat), }, @@ -159,10 +167,12 @@ export default class ProblemList extends PureComponent } expanded={this.getExpandedPage(this.state.page)} @@ -176,14 +186,41 @@ export default class ProblemList extends PureComponent, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) { +interface HostCellProps { + name: string; + maintenance: boolean; +} + +const HostCell: React.FC = ({ name, maintenance }) => { + return ( +
+ {name} + {maintenance && } +
+ ); +}; + +function SeverityCell( + props: RTCell, + problemSeverityDesc: TriggerSeverity[], + markAckEvents?: boolean, + ackEventColor?: string, + okColor = DEFAULT_OK_COLOR + ) { const problem = props.original; let color: string; - const severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(props.original.priority)); - color = severityDesc.color; + + let severityDesc: TriggerSeverity; + const severity = Number(problem.severity); + severityDesc = _.find(problemSeverityDesc, s => s.priority === severity); + if (problem.severity && problem.value === '1') { + severityDesc = _.find(problemSeverityDesc, s => s.priority === severity); + } + + color = problem.value === '0' ? okColor : severityDesc.color; // Mark acknowledged triggers with different color - if (markAckEvents && problem.lastEvent.acknowledged === "1") { + if (markAckEvents && problem.acknowledged === "1") { color = ackEventColor; } @@ -197,9 +234,9 @@ function SeverityCell(props: RTCell, problemSeverityDesc: TriggerSev const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; -function StatusCell(props: RTCell, okColor = DEFAULT_OK_COLOR, problemColor = DEFAULT_PROBLEM_COLOR, highlightNewerThan?: string) { +function StatusCell(props: RTCell, highlightNewerThan?: string) { const status = props.value === '0' ? 'RESOLVED' : 'PROBLEM'; - const color = props.value === '0' ? okColor : problemColor; + const color = props.value === '0' ? DEFAULT_OK_COLOR : DEFAULT_PROBLEM_COLOR; let newProblem = false; if (highlightNewerThan) { newProblem = isNewProblem(props.original, highlightNewerThan); @@ -209,7 +246,7 @@ function StatusCell(props: RTCell, okColor = DEFAULT_OK_COLOR, probl ); } -function StatusIconCell(props: RTCell, highlightNewerThan?: string) { +function StatusIconCell(props: RTCell, highlightNewerThan?: string) { const status = props.value === '0' ? 'ok' : 'problem'; let newProblem = false; if (highlightNewerThan) { @@ -223,7 +260,7 @@ function StatusIconCell(props: RTCell, highlightNewerThan?: string) return ; } -function GroupCell(props: RTCell) { +function GroupCell(props: RTCell) { let groups = ""; if (props.value && props.value.length) { groups = props.value.map(g => g.name).join(', '); @@ -233,7 +270,7 @@ function GroupCell(props: RTCell) { ); } -function ProblemCell(props: RTCell) { +function ProblemCell(props: RTCell) { const comments = props.original.comments; return (
@@ -243,23 +280,23 @@ function ProblemCell(props: RTCell) { ); } -function AgeCell(props: RTCell) { +function AgeCell(props: RTCell) { const problem = props.original; - const timestamp = moment.unix(problem.lastchangeUnix); + const timestamp = moment.unix(problem.timestamp); const age = timestamp.fromNow(true); return {age}; } -function LastChangeCell(props: RTCell, customFormat?: string) { +function LastChangeCell(props: RTCell, customFormat?: string) { const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; const problem = props.original; - const timestamp = moment.unix(problem.lastchangeUnix); + const timestamp = moment.unix(problem.timestamp); const format = customFormat || DEFAULT_TIME_FORMAT; const lastchange = timestamp.format(format); return {lastchange}; } -interface TagCellProps extends RTCell { +interface TagCellProps extends RTCell { onTagClick: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; } diff --git a/src/panel-triggers/components/Tooltip/Popper.tsx b/src/panel-triggers/components/Tooltip/Popper.tsx deleted file mode 100644 index f3b451b..0000000 --- a/src/panel-triggers/components/Tooltip/Popper.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { PureComponent } from 'react'; -import classNames from 'classnames'; -import BodyPortal from './Portal'; -import { Manager, Popper as ReactPopper, Reference } from 'react-popper'; -import Transition from 'react-transition-group/Transition'; - -const defaultTransitionStyles = { - transition: 'opacity 200ms linear', - opacity: 0, -}; - -const transitionStyles = { - exited: { opacity: 0 }, - entering: { opacity: 0 }, - entered: { opacity: 1 }, - exiting: { opacity: 0 }, -}; - -interface Props { - renderContent: (content: any) => any; - show: boolean; - placement?: any; - content: string | ((props: any) => JSX.Element); - refClassName?: string; - popperClassName?: string; -} - -class Popper extends PureComponent { - render() { - const { children, renderContent, show, placement, refClassName } = this.props; - const { content } = this.props; - const popperClassName = classNames('popper', this.props.popperClassName); - - return ( - - - {({ ref }) => ( -
- {children} -
- )} -
- - {transitionState => ( - - - {({ ref, style, placement, arrowProps }) => { - return ( -
-
- {renderContent(content)} -
-
-
- ); - }} - - - )} - - - ); - } -} - -export default Popper; diff --git a/src/panel-triggers/components/Tooltip/Tooltip.tsx b/src/panel-triggers/components/Tooltip/Tooltip.tsx deleted file mode 100644 index 1f5fc05..0000000 --- a/src/panel-triggers/components/Tooltip/Tooltip.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { PureComponent } from 'react'; -import Popper from './Popper'; -import withPopper, { UsingPopperProps } from './withPopper'; - -class Tooltip extends PureComponent { - render() { - const { children, hidePopper, showPopper, className, ...restProps } = this.props; - - return ( -
- {children} -
- ); - } -} - -export default withPopper(Tooltip); diff --git a/src/panel-triggers/datasource-selector.directive.js b/src/panel-triggers/datasource-selector.directive.js deleted file mode 100644 index 0d55483..0000000 --- a/src/panel-triggers/datasource-selector.directive.js +++ /dev/null @@ -1,57 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; - -const template = ` - - -`; - -angular -.module('grafana.directives') -.directive('datasourceSelector', () => { - return { - scope: { - datasources: "=", - options: "=", - onChange: "&" - }, - controller: DatasourceSelectorCtrl, - controllerAs: 'ctrl', - template: template - }; -}); - -class DatasourceSelectorCtrl { - - /** @ngInject */ - constructor($scope) { - this.scope = $scope; - let datasources = $scope.datasources; - let options = $scope.options; - this.dsOptions = { - multi: true, - current: {value: datasources, text: datasources.join(" + ")}, - options: _.map(options, (ds) => { - return {text: ds, value: ds, selected: _.includes(datasources, ds)}; - }) - }; - // Fix for Grafana 6.0 - // https://github.com/grafana/grafana/blob/v6.0.0/public/app/core/directives/value_select_dropdown.ts#L291 - this.dashboard = { - on: () => {} - }; - } - - onChange(updatedOptions) { - let newDataSources = updatedOptions.current.value; - this.scope.datasources = newDataSources; - - // Run after model was changed - this.scope.$$postDigest(() => { - this.scope.onChange(); - }); - } -} diff --git a/src/panel-triggers/img/icn-zabbix-problems-panel.svg b/src/panel-triggers/img/icn-zabbix-problems-panel.svg new file mode 100644 index 0000000..990c421 --- /dev/null +++ b/src/panel-triggers/img/icn-zabbix-problems-panel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/panel-triggers/migrations.ts b/src/panel-triggers/migrations.ts index f929160..770c249 100644 --- a/src/panel-triggers/migrations.ts +++ b/src/panel-triggers/migrations.ts @@ -1,9 +1,27 @@ import _ from 'lodash'; import { getNextRefIdChar } from './utils'; -import { getDefaultTarget } from './triggers_panel_ctrl'; +import { ShowProblemTypes } from '../datasource-zabbix/types'; // Actual schema version -export const CURRENT_SCHEMA_VERSION = 7; +export const CURRENT_SCHEMA_VERSION = 8; + +export const getDefaultTarget = (targets?) => { + return { + group: {filter: ""}, + host: {filter: ""}, + application: {filter: ""}, + trigger: {filter: ""}, + tags: {filter: ""}, + proxy: {filter: ""}, + refId: getNextRefIdChar(targets), + }; +}; + +export function getDefaultTargetOptions() { + return { + hostsInMaintenance: true, + }; +} export function migratePanelSchema(panel) { if (isEmptyPanel(panel)) { @@ -12,7 +30,7 @@ export function migratePanelSchema(panel) { } const schemaVersion = getSchemaVersion(panel); - panel.schemaVersion = CURRENT_SCHEMA_VERSION; + // panel.schemaVersion = CURRENT_SCHEMA_VERSION; if (schemaVersion < 2) { panel.datasources = [panel.datasource]; @@ -66,9 +84,70 @@ export function migratePanelSchema(panel) { delete panel.datasources; } + if (schemaVersion < 8) { + if (panel.targets.length === 1) { + if (panel.targets[0].datasource) { + panel.datasource = panel.targets[0].datasource; + delete panel.targets[0].datasource; + } + } else if (panel.targets.length > 1) { + // Mixed data sources + panel.datasource = '-- Mixed --'; + } + for (const target of panel.targets) { + // set queryType to PROBLEMS + target.queryType = 5; + target.showProblems = migrateShowEvents(panel); + target.options = migrateOptions(panel); + + _.defaults(target.options, getDefaultTargetOptions()); + _.defaults(target, { tags: { filter: "" } }); + } + + panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange'; + + delete panel.showEvents; + delete panel.showTriggers; + delete panel.hostsInMaintenance; + delete panel.sortTriggersBy; + } + return panel; } +function migrateOptions(panel) { + let acknowledged = 2; + if (panel.showTriggers === 'acknowledged') { + acknowledged = 1; + } else if (panel.showTriggers === 'unacknowledged') { + acknowledged = 0; + } + + // Default limit in Zabbix + let limit = 1001; + if (panel.limit && panel.limit !== 100) { + limit = panel.limit; + } + + return { + hostsInMaintenance: panel.hostsInMaintenance, + sortProblems: panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'default', + minSeverity: 0, + acknowledged: acknowledged, + limit: limit, + }; +} + +function migrateShowEvents(panel) { + if (panel.showEvents?.value === 1) { + return ShowProblemTypes.Problems; + } else if (panel.showEvents?.value === 0 || panel.showEvents?.value?.length > 1) { + return ShowProblemTypes.History; + } else { + return ShowProblemTypes.Problems; + } +} + function getSchemaVersion(panel) { return panel.schemaVersion || 1; } diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js index 2ea2c17..a6f7f15 100644 --- a/src/panel-triggers/module.js +++ b/src/panel-triggers/module.js @@ -11,9 +11,8 @@ * Licensed under the Apache License, Version 2.0 */ -import {TriggerPanelCtrl} from './triggers_panel_ctrl'; -import {loadPluginCss} from 'grafana/app/plugins/sdk'; -import './datasource-selector.directive'; +import { TriggerPanelCtrl } from './triggers_panel_ctrl'; +import { loadPluginCss } from 'grafana/app/plugins/sdk'; loadPluginCss({ dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', diff --git a/src/panel-triggers/options_tab.js b/src/panel-triggers/options_tab.js index acf7c50..c67521f 100644 --- a/src/panel-triggers/options_tab.js +++ b/src/panel-triggers/options_tab.js @@ -29,10 +29,13 @@ class TriggerPanelOptionsCtrl { 'unacknowledged', 'acknowledged' ]; - this.sortByFields = [ - { text: 'last change', value: 'lastchange' }, - { text: 'severity', value: 'priority' } + + this.sortingOptions = [ + { text: 'Default', value: 'default' }, + { text: 'Last change', value: 'lastchange' }, + { text: 'Severity', value: 'priority' }, ]; + this.showEventsFields = [ { text: 'All', value: [0,1] }, { text: 'OK', value: [0] }, diff --git a/src/panel-triggers/partials/options_tab.html b/src/panel-triggers/partials/options_tab.html index b7253f0..378bb08 100644 --- a/src/panel-triggers/partials/options_tab.html +++ b/src/panel-triggers/partials/options_tab.html @@ -1,6 +1,6 @@
-
Show fields
+
Fields
+ on-change="ctrl.render()"> + +
-
-
Options
- - -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- - -
-
-
View options
@@ -130,6 +89,16 @@ ng-change="ctrl.render()">
+
+ +
+ +
+
@@ -202,7 +171,7 @@
-
Triggers severity and colors
+
Problems severity and colors
@@ -224,7 +193,7 @@ label-class="width-0" label="Show" checked="trigger.show" - on-change="ctrl.refresh()"> + on-change="ctrl.reRenderProblems()">
@@ -246,7 +215,7 @@ label-class="width-0" label="Show" checked="ctrl.panel.markAckEvents" - on-change="ctrl.refresh()"> + on-change="ctrl.reRenderProblems()">
diff --git a/src/panel-triggers/partials/triggers_tab.html b/src/panel-triggers/partials/triggers_tab.html deleted file mode 100644 index e7df9fe..0000000 --- a/src/panel-triggers/partials/triggers_tab.html +++ /dev/null @@ -1,104 +0,0 @@ -
-
-
-
- -
-
- - -
-
-
-
- -
-
-
{{ target.datasource }}
-
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
-
-
diff --git a/src/panel-triggers/plugin.json b/src/panel-triggers/plugin.json index 6639999..68cc9a6 100644 --- a/src/panel-triggers/plugin.json +++ b/src/panel-triggers/plugin.json @@ -3,13 +3,14 @@ "name": "Zabbix Problems", "id": "alexanderzobnin-zabbix-triggers-panel", - "dataFormats": [], - "skipDataQuery": true, - "info": { "author": { "name": "Alexander Zobnin", "url": "https://github.com/alexanderzobnin/grafana-zabbix" + }, + "logos": { + "small": "img/icn-zabbix-problems-panel.svg", + "large": "img/icn-zabbix-problems-panel.svg" } } } diff --git a/src/panel-triggers/specs/migrations.spec.ts b/src/panel-triggers/specs/migrations.spec.ts index bd916e8..189ffe7 100644 --- a/src/panel-triggers/specs/migrations.spec.ts +++ b/src/panel-triggers/specs/migrations.spec.ts @@ -1,18 +1,23 @@ import _ from 'lodash'; import mocks from '../../test-setup/mocks'; import {TriggerPanelCtrl} from '../triggers_panel_ctrl'; -import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl'; -import {CURRENT_SCHEMA_VERSION} from '../migrations'; +import { DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS } from '../triggers_panel_ctrl'; +import { CURRENT_SCHEMA_VERSION } from '../migrations'; + +jest.mock('@grafana/runtime', () => { + return { + getDataSourceSrv: () => ({ + getMetricSources: () => { + return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }]; + }, + get: () => Promise.resolve({}) + }), + }; +}, {virtual: true}); describe('Triggers Panel schema migration', () => { let ctx: any = {}; let updatePanelCtrl; - const datasourceSrvMock = { - getMetricSources: () => { - return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }]; - }, - get: () => Promise.resolve({}) - }; const timeoutMock = () => {}; @@ -29,8 +34,9 @@ describe('Triggers Panel schema migration', () => { ageField: true, infoField: true, limit: 10, - showTriggers: 'all triggers', + showTriggers: 'unacknowledged', hideHostsInMaintenance: false, + hostsInMaintenance: false, sortTriggersBy: { text: 'last change', value: 'lastchange' }, showEvents: { text: 'Problems', value: '1' }, triggerSeverity: DEFAULT_SEVERITY, @@ -43,7 +49,7 @@ describe('Triggers Panel schema migration', () => { } }; - updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock); + updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock); }); it('should update old panel schema', () => { @@ -51,12 +57,22 @@ describe('Triggers Panel schema migration', () => { const expected = _.defaultsDeep({ schemaVersion: CURRENT_SCHEMA_VERSION, + datasource: 'zabbix', targets: [ { ...DEFAULT_TARGET, - datasource: 'zabbix', + queryType: 5, + showProblems: 'problems', + options: { + hostsInMaintenance: false, + acknowledged: 0, + sortProblems: 'default', + minSeverity: 0, + limit: 10, + }, } ], + sortProblems: 'lastchange', ageField: true, statusField: false, severityField: false, @@ -74,27 +90,7 @@ describe('Triggers Panel schema migration', () => { const expected = _.defaultsDeep({ schemaVersion: CURRENT_SCHEMA_VERSION, - targets: [{ - ...DEFAULT_TARGET, - datasource: 'zabbix_default' - }] }, PANEL_DEFAULTS); expect(updatedPanelCtrl.panel).toEqual(expected); }); - - it('should set default targets for new panel with empty targets', () => { - ctx.scope.panel = { - targets: [] - }; - const updatedPanelCtrl = updatePanelCtrl(ctx.scope); - - const expected = _.defaultsDeep({ - targets: [{ - ...DEFAULT_TARGET, - datasource: 'zabbix_default' - }] - }, PANEL_DEFAULTS); - - expect(updatedPanelCtrl.panel).toEqual(expected); - }); }); diff --git a/src/panel-triggers/specs/panel_ctrl.spec.ts b/src/panel-triggers/specs/panel_ctrl.spec.ts index 67283c9..01ac557 100644 --- a/src/panel-triggers/specs/panel_ctrl.spec.ts +++ b/src/panel-triggers/specs/panel_ctrl.spec.ts @@ -1,90 +1,53 @@ import _ from 'lodash'; -import mocks from '../../test-setup/mocks'; -import {TriggerPanelCtrl} from '../triggers_panel_ctrl'; -import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl'; -// import { create } from 'domain'; +import { TriggerPanelCtrl } from '../triggers_panel_ctrl'; +import { PANEL_DEFAULTS, DEFAULT_TARGET } from '../triggers_panel_ctrl'; + +let datasourceSrvMock, zabbixDSMock; + +jest.mock('@grafana/runtime', () => { + return { + getDataSourceSrv: () => datasourceSrvMock, + }; +}, {virtual: true}); describe('TriggerPanelCtrl', () => { let ctx: any = {}; - let datasourceSrvMock, zabbixDSMock; - const timeoutMock = () => {}; - let createPanelCtrl; + let createPanelCtrl: () => any; beforeEach(() => { - ctx = {scope: {panel: PANEL_DEFAULTS}}; + ctx = { scope: { + panel: { + ...PANEL_DEFAULTS, + sortProblems: 'lastchange', + } + }}; + ctx.scope.panel.targets = [{ + ...DEFAULT_TARGET, + datasource: 'zabbix_default', + }]; + zabbixDSMock = { - replaceTemplateVars: () => {}, zabbix: { - getTriggers: jest.fn().mockReturnValue([generateTrigger("1"), generateTrigger("1")]), getExtendedEventData: jest.fn().mockResolvedValue([]), getEventAlerts: jest.fn().mockResolvedValue([]), } }; datasourceSrvMock = { - getMetricSources: () => { - return [ - { meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }, - { meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix' }, - { meta: {id: 'graphite'}, value: {}, name: 'graphite' }, - ]; - }, get: () => Promise.resolve(zabbixDSMock) }; - createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock); - const getTriggersResp = [ - [ - createTrigger({ - triggerid: "1", lastchange: "1510000010", priority: 5, lastEvent: {eventid: "11"}, hosts: [{maintenance_status: '1'}] - }), - createTrigger({ - triggerid: "2", lastchange: "1510000040", priority: 3, lastEvent: {eventid: "12"} - }), - ], - [ - createTrigger({triggerid: "3", lastchange: "1510000020", priority: 4, lastEvent: {eventid: "13"}}), - createTrigger({triggerid: "4", lastchange: "1510000030", priority: 2, lastEvent: {eventid: "14"}}), - ] - ]; - - // Simulate 2 data sources - zabbixDSMock.zabbix.getTriggers = jest.fn() - .mockReturnValueOnce(getTriggersResp[0]) - .mockReturnValueOnce(getTriggersResp[1]); - zabbixDSMock.zabbix.getExtendedEventData = jest.fn() - .mockReturnValue(Promise.resolve([defaultEvent])); + const timeoutMock = (fn: () => any) => Promise.resolve(fn()); + createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock); ctx.panelCtrl = createPanelCtrl(); - }); - describe('When adding new panel', () => { - it('should suggest all zabbix data sources', () => { - ctx.scope.panel = {}; - const panelCtrl = createPanelCtrl(); - expect(panelCtrl.available_datasources).toEqual([ - 'zabbix_default', 'zabbix' - ]); - }); - - it('should load first zabbix data source as default', () => { - ctx.scope.panel = {}; - const panelCtrl = createPanelCtrl(); - expect(panelCtrl.panel.targets[0].datasource).toEqual('zabbix_default'); - }); - - it('should rewrite default empty target', () => { - ctx.scope.panel = { - targets: [{ - "target": "", - "refId": "A" - }], - }; - const panelCtrl = createPanelCtrl(); - expect(panelCtrl.available_datasources).toEqual([ - 'zabbix_default', 'zabbix' - ]); - }); + ctx.dataFramesReceived = generateDataFramesResponse([ + {id: "1", lastchange: "1510000010", priority: 5}, + {id: "2", lastchange: "1510000040", priority: 3}, + {id: "3", lastchange: "1510000020", priority: 4}, + {id: "4", lastchange: "1510000030", priority: 2}, + ]); }); describe('When refreshing panel', () => { @@ -104,8 +67,8 @@ describe('TriggerPanelCtrl', () => { }); it('should format triggers', (done) => { - ctx.panelCtrl.onRefresh().then(() => { - const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"}); + ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { + const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, {triggerid: "1"}); expect(formattedTrigger.host).toBe('backend01'); expect(formattedTrigger.hostTechName).toBe('backend01_tech'); expect(formattedTrigger.datasource).toBe('zabbix_default'); @@ -116,8 +79,8 @@ describe('TriggerPanelCtrl', () => { }); it('should sort triggers by time by default', (done) => { - ctx.panelCtrl.onRefresh().then(() => { - const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid'); + ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { + const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid'); expect(trigger_ids).toEqual([ '2', '4', '3', '1' ]); @@ -126,175 +89,119 @@ describe('TriggerPanelCtrl', () => { }); it('should sort triggers by severity', (done) => { - ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' }; - ctx.panelCtrl.onRefresh().then(() => { - const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid'); + ctx.panelCtrl.panel.sortProblems = 'priority'; + ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { + const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid'); expect(trigger_ids).toEqual([ '1', '3', '2', '4' ]); done(); }); }); - - it('should add acknowledges to trigger', (done) => { - ctx.panelCtrl.onRefresh().then(() => { - const trigger = getTriggerById(1, ctx); - expect(trigger.acknowledges).toHaveLength(1); - expect(trigger.acknowledges[0].message).toBe("event ack"); - - expect(getTriggerById(2, ctx).acknowledges).toBe(undefined); - expect(getTriggerById(3, ctx).acknowledges).toBe(undefined); - expect(getTriggerById(4, ctx).acknowledges).toBe(undefined); - done(); - }); - }); - }); - - describe('When formatting triggers', () => { - beforeEach(() => { - ctx.panelCtrl = createPanelCtrl(); - }); - - it('should handle new lines in trigger description', () => { - ctx.panelCtrl.setTriggerSeverity = jest.fn((trigger) => trigger); - const trigger = {comments: "this is\ndescription"}; - const formattedTrigger = ctx.panelCtrl.formatTrigger(trigger); - expect(formattedTrigger.comments).toBe("this is
description"); - }); - - it('should format host name to display (default)', (done) => { - ctx.panelCtrl.onRefresh().then(() => { - const trigger = getTriggerById(1, ctx); - const hostname = ctx.panelCtrl.formatHostName(trigger); - expect(hostname).toBe('backend01'); - done(); - }); - }); - - it('should format host name to display (tech name)', (done) => { - ctx.panelCtrl.panel.hostField = false; - ctx.panelCtrl.panel.hostTechNameField = true; - ctx.panelCtrl.onRefresh().then(() => { - const trigger = getTriggerById(1, ctx); - const hostname = ctx.panelCtrl.formatHostName(trigger); - expect(hostname).toBe('backend01_tech'); - done(); - }); - }); - - it('should format host name to display (both tech and visible)', (done) => { - ctx.panelCtrl.panel.hostField = true; - ctx.panelCtrl.panel.hostTechNameField = true; - ctx.panelCtrl.onRefresh().then(() => { - const trigger = getTriggerById(1, ctx); - const hostname = ctx.panelCtrl.formatHostName(trigger); - expect(hostname).toBe('backend01 (backend01_tech)'); - done(); - }); - }); - - it('should hide hostname if both visible and tech name checkboxes unset', (done) => { - ctx.panelCtrl.panel.hostField = false; - ctx.panelCtrl.panel.hostTechNameField = false; - ctx.panelCtrl.onRefresh().then(() => { - const trigger = getTriggerById(1, ctx); - const hostname = ctx.panelCtrl.formatHostName(trigger); - expect(hostname).toBe(""); - done(); - }); - }); - }); - - describe('When formatting acknowledges', () => { - beforeEach(() => { - ctx.panelCtrl = createPanelCtrl(); - }); - - it('should build proper user name', () => { - const ack = { - alias: 'alias', name: 'name', surname: 'surname' - }; - - const formatted = ctx.panelCtrl.formatAcknowledge(ack); - expect(formatted.user).toBe('alias (name surname)'); - }); - - it('should return empty name if it is not defined', () => { - const formatted = ctx.panelCtrl.formatAcknowledge({}); - expect(formatted.user).toBe(''); - }); }); }); -const defaultTrigger: any = { - "triggerid": "13565", - "value": "1", - "groups": [{"groupid": "1", "name": "Backend"}] , - "hosts": [{"host": "backend01_tech", "hostid": "10001","maintenance_status": "0", "name": "backend01"}] , - "lastEvent": { - "eventid": "11", - "clock": "1507229064", - "ns": "556202037", - "acknowledged": "1", - "value": "1", - "object": "0", - "source": "0", - "objectid": "13565", - }, - "tags": [] , - "lastchange": "1440259530", - "priority": "2", - "description": "Lack of free swap space on server", +const defaultProblem: any = { + "acknowledges": [], "comments": "It probably means that the systems requires\nmore physical memory.", - "url": "https://host.local/path", - "templateid": "0", "expression": "{13174}<50", "manual_close": "0", "correlation_mode": "0", - "correlation_tag": "", "recovery_mode": "0", "recovery_expression": "", "state": "0", "status": "0", - "flags": "0", "type": "0", "items": [] , "error": "" -}; - -const defaultEvent: any = { - "eventid": "11", - "acknowledges": [ + "correlation_mode": "0", + "correlation_tag": "", + "datasource": "zabbix_default", + "description": "Lack of free swap space on server", + "error": "", + "expression": "{13297}>20", + "flags": "0", + "groups": [ { - "acknowledgeid": "185", - "action": "0", - "alias": "api", - "clock": "1512382246", - "eventid": "11", - "message": "event ack", - "name": "api", - "surname": "user", - "userid": "3" + "groupid": "2", + "name": "Linux servers" + }, + { + "groupid": "9", + "name": "Backend" } ], - "clock": "1507229064", - "ns": "556202037", - "acknowledged": "1", - "value": "1", - "object": "0", - "source": "0", - "objectid": "1", + "hosts": [ + { + "host": "backend01_tech", + "hostid": "10111", + "maintenance_status": "1", + "name": "backend01", + "proxy_hostid": "0" + } + ], + "items": [ + { + "itemid": "23979", + "key_": "system.cpu.util[,iowait]", + "lastvalue": "25.2091", + "name": "CPU $2 time" + } + ], + "lastEvent": { + "acknowledged": "0", + "clock": "1589297010", + "eventid": "4399289", + "name": "Disk I/O is overloaded on backend01", + "ns": "224779201", + "object": "0", + "objectid": "13682", + "severity": "2", + "source": "0", + "value": "1" + }, + "lastchange": "1440259530", + "maintenance": true, + "manual_close": "0", + "priority": "2", + "recovery_expression": "", + "recovery_mode": "0", + "showAckButton": true, + "state": "0", + "status": "0", + "tags": [], + "templateid": "13671", + "triggerid": "13682", + "type": "0", + "url": "", + "value": "1" }; -function generateTrigger(id, timestamp?, severity?): any { - const trigger = _.cloneDeep(defaultTrigger); - trigger.triggerid = id.toString(); +function generateDataFramesResponse(problemDescs: any[] = [{id: 1}]): any { + const problems = problemDescs.map(problem => generateProblem(problem.id, problem.lastchange, problem.priority)); + + return [ + { + "fields": [ + { + "config": {}, + "name": "Problems", + "state": { + "scopedVars": {}, + "title": null + }, + "type": "other", + "values": problems, + } + ], + "length": 16, + "name": "problems" + } + ]; +} + +function generateProblem(id, timestamp?, severity?): any { + const problem = _.cloneDeep(defaultProblem); + problem.triggerid = id.toString(); if (severity) { - trigger.priority = severity.toString(); + problem.priority = severity.toString(); } if (timestamp) { - trigger.lastchange = timestamp; + problem.lastchange = timestamp; } - return trigger; + return problem; } -function createTrigger(props): any { - let trigger = _.cloneDeep(defaultTrigger); - trigger = _.merge(trigger, props); - trigger.lastEvent.objectid = trigger.triggerid; - return trigger; -} - -function getTriggerById(id, ctx): any { - return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()}); +function getProblemById(id, ctx): any { + return _.find(ctx.panelCtrl.renderData, {triggerid: id.toString()}); } diff --git a/src/panel-triggers/triggers_panel_ctrl.js b/src/panel-triggers/triggers_panel_ctrl.js deleted file mode 100644 index 23c6011..0000000 --- a/src/panel-triggers/triggers_panel_ctrl.js +++ /dev/null @@ -1,725 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import _ from 'lodash'; -import moment from 'moment'; -import * as dateMath from 'grafana/app/core/utils/datemath'; -import * as utils from '../datasource-zabbix/utils'; -import { PanelCtrl } from 'grafana/app/plugins/sdk'; -import { triggerPanelOptionsTab } from './options_tab'; -import { triggerPanelTriggersTab } from './triggers_tab'; -import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations'; -import ProblemList from './components/Problems/Problems'; -import AlertList from './components/AlertList/AlertList'; -import { getNextRefIdChar } from './utils'; - -const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource'; -const PROBLEM_EVENTS_LIMIT = 100; - -export const DEFAULT_TARGET = { - group: {filter: ""}, - host: {filter: ""}, - application: {filter: ""}, - trigger: {filter: ""}, - tags: {filter: ""}, - proxy: {filter: ""}, -}; - -export const getDefaultTarget = (targets) => { - return { - group: {filter: ""}, - host: {filter: ""}, - application: {filter: ""}, - trigger: {filter: ""}, - tags: {filter: ""}, - proxy: {filter: ""}, - refId: getNextRefIdChar(targets), - }; -}; - -export const DEFAULT_SEVERITY = [ - { priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true}, - { priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true}, - { priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true}, - { priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true}, - { priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true}, - { priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true}, -]; - -export const getDefaultSeverity = () => DEFAULT_SEVERITY; - -const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; - -export const PANEL_DEFAULTS = { - schemaVersion: CURRENT_SCHEMA_VERSION, - targets: [getDefaultTarget([])], - // Fields - hostField: true, - hostTechNameField: false, - hostGroups: false, - hostProxy: false, - showTags: true, - statusField: true, - statusIcon: false, - severityField: true, - ageField: false, - descriptionField: true, - descriptionAtNewLine: false, - // Options - hostsInMaintenance: true, - showTriggers: 'all triggers', - sortTriggersBy: { text: 'last change', value: 'lastchange' }, - showEvents: { text: 'Problems', value: 1 }, - limit: 100, - // View options - layout: 'table', - fontSize: '100%', - pageSize: 10, - problemTimeline: true, - highlightBackground: false, - highlightNewEvents: false, - highlightNewerThan: '1h', - customLastChangeFormat: false, - lastChangeFormat: "", - resizedColumns: [], - // Triggers severity and colors - triggerSeverity: getDefaultSeverity(), - okEventColor: 'rgb(56, 189, 113)', - ackEventColor: 'rgb(56, 219, 156)', - markAckEvents: false, -}; - -const triggerStatusMap = { - '0': 'OK', - '1': 'PROBLEM' -}; - -export class TriggerPanelCtrl extends PanelCtrl { - - /** @ngInject */ - constructor($scope, $injector, $timeout, datasourceSrv, templateSrv, contextSrv, dashboardSrv, timeSrv) { - super($scope, $injector); - this.datasourceSrv = datasourceSrv; - this.templateSrv = templateSrv; - this.contextSrv = contextSrv; - this.dashboardSrv = dashboardSrv; - this.timeSrv = timeSrv; - this.scope = $scope; - this.$timeout = $timeout; - - this.editorTabIndex = 1; - this.triggerStatusMap = triggerStatusMap; - this.defaultTimeFormat = DEFAULT_TIME_FORMAT; - this.pageIndex = 0; - this.triggerList = []; - this.datasources = {}; - this.range = {}; - - this.panel = migratePanelSchema(this.panel); - _.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS)); - - this.available_datasources = _.map(this.getZabbixDataSources(), 'name'); - if (this.panel.targets && !this.panel.targets[0].datasource) { - this.panel.targets[0].datasource = this.available_datasources[0]; - } - - this.initDatasources(); - this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); - this.events.on('refresh', this.onRefresh.bind(this)); - } - - setPanelError(err, defaultError) { - const defaultErrorMessage = defaultError || "Request Error"; - this.inspector = { error: err }; - this.error = err.message || defaultErrorMessage; - if (err.data) { - if (err.data.message) { - this.error = err.data.message; - } - if (err.data.error) { - this.error = err.data.error; - } - } - - this.events.emit('data-error', err); - console.log('Panel data error:', err); - } - - initDatasources() { - if (!this.panel.targets) { - return; - } - const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource)); - let promises = targetDatasources.map(ds => { - // Load datasource - return this.datasourceSrv.get(ds) - .then(datasource => { - this.datasources[ds] = datasource; - return datasource; - }); - }); - return Promise.all(promises); - } - - getZabbixDataSources() { - return _.filter(this.datasourceSrv.getMetricSources(), datasource => { - return datasource.meta.id === ZABBIX_DS_ID && datasource.value; - }); - } - - isEmptyTargets() { - const emptyTargets = _.isEmpty(this.panel.targets); - const emptyTarget = (this.panel.targets.length === 1 && ( - _.isEmpty(this.panel.targets[0]) || - this.panel.targets[0].target === "" - )); - return emptyTargets || emptyTarget; - } - - onInitEditMode() { - this.addEditorTab('Triggers', triggerPanelTriggersTab, 1); - this.addEditorTab('Options', triggerPanelOptionsTab, 2); - } - - setTimeQueryStart() { - this.timing.queryStart = new Date().getTime(); - } - - setTimeQueryEnd() { - this.timing.queryEnd = (new Date()).getTime(); - } - - onRefresh() { - // ignore fetching data if another panel is in fullscreen - if (this.otherPanelInFullscreenMode()) { return; } - - this.range = this.timeSrv.timeRange(); - - // clear loading/error state - delete this.error; - this.loading = true; - this.setTimeQueryStart(); - this.pageIndex = 0; - - return this.getTriggers() - .then(triggers => { - // Notify panel that request is finished - this.loading = false; - this.setTimeQueryEnd(); - return this.renderTriggers(triggers); - }) - .then(() => { - this.$timeout(() => { - this.renderingCompleted(); - }); - }) - .catch(err => { - this.loading = false; - - if (err.cancelled) { - console.log('Panel request cancelled', err); - return; - } - - this.setPanelError(err); - }); - } - - renderTriggers(zabbixTriggers) { - let triggers = _.cloneDeep(zabbixTriggers || this.triggerListUnfiltered); - this.triggerListUnfiltered = _.cloneDeep(triggers); - - triggers = _.map(triggers, this.formatTrigger.bind(this)); - triggers = this.filterTriggersPost(triggers); - triggers = this.sortTriggers(triggers); - - // Limit triggers number - triggers = triggers.slice(0, this.panel.limit || PANEL_DEFAULTS.limit); - - this.triggerList = triggers; - - return this.$timeout(() => { - return super.render(this.triggerList); - }); - } - - getTriggers() { - const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); - const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); - const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin; - - let promises = _.map(this.panel.targets, (target) => { - const ds = target.datasource; - let proxies; - let showAckButton = true; - return this.datasourceSrv.get(ds) - .then(datasource => { - const zabbix = datasource.zabbix; - const showEvents = this.panel.showEvents.value; - const triggerFilter = target; - const showProxy = this.panel.hostProxy; - const getProxiesPromise = showProxy ? zabbix.getProxies() : () => []; - showAckButton = !datasource.disableReadOnlyUsersAck || userIsEditor; - - // Replace template variables - const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter); - const hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); - const appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); - const proxyFilter = datasource.replaceTemplateVars(triggerFilter.proxy.filter); - - let triggersOptions = { - showTriggers: showEvents - }; - - if (showEvents !== 1) { - triggersOptions.timeFrom = timeFrom; - triggersOptions.timeTo = timeTo; - } - - return Promise.all([ - zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions, proxyFilter), - getProxiesPromise - ]); - }).then(([triggers, sourceProxies]) => { - proxies = _.keyBy(sourceProxies, 'proxyid'); - const eventids = _.compact(triggers.map(trigger => { - return trigger.lastEvent.eventid; - })); - return Promise.all([ - this.datasources[ds].zabbix.getExtendedEventData(eventids), - Promise.resolve(triggers) - ]); - }) - .then(([events, triggers]) => { - this.addEventTags(events, triggers); - this.addAcknowledges(events, triggers); - return triggers; - }) - .then(triggers => this.setMaintenanceStatus(triggers)) - .then(triggers => this.setAckButtonStatus(triggers, showAckButton)) - .then(triggers => this.filterTriggersPre(triggers, target)) - .then(triggers => this.addTriggerDataSource(triggers, target)) - .then(triggers => this.addTriggerHostProxy(triggers, proxies)); - }); - - return Promise.all(promises) - .then(results => _.flatten(results)); - } - - addAcknowledges(events, triggers) { - // Map events to triggers - _.each(triggers, trigger => { - var event = _.find(events, event => { - return event.eventid === trigger.lastEvent.eventid; - }); - - if (event) { - trigger.acknowledges = _.map(event.acknowledges, this.formatAcknowledge.bind(this)); - } - - if (!trigger.lastEvent.eventid) { - trigger.lastEvent = null; - } - }); - - return triggers; - } - - formatAcknowledge(ack) { - let timestamp = moment.unix(ack.clock); - if (this.panel.customLastChangeFormat) { - ack.time = timestamp.format(this.panel.lastChangeFormat); - } else { - ack.time = timestamp.format(this.defaultTimeFormat); - } - ack.user = ack.alias || ''; - if (ack.name || ack.surname) { - const fullName = `${ack.name || ''} ${ack.surname || ''}`; - ack.user += ` (${fullName})`; - } - return ack; - } - - addEventTags(events, triggers) { - _.each(triggers, trigger => { - var event = _.find(events, event => { - return event.eventid === trigger.lastEvent.eventid; - }); - if (event && event.tags && event.tags.length) { - trigger.tags = event.tags; - } - }); - return triggers; - } - - filterTriggersPre(triggerList, target) { - // Filter triggers by description - const ds = target.datasource; - let triggerFilter = target.trigger.filter; - triggerFilter = this.datasources[ds].replaceTemplateVars(triggerFilter); - if (triggerFilter) { - triggerList = filterTriggers(triggerList, triggerFilter); - } - - // Filter by tags - // const target = this.panel.targets[ds]; - if (target.tags.filter) { - let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter); - // replaceTemplateVars() builds regex-like string, so we should trim it. - tagsFilter = tagsFilter.replace('/^', '').replace('$/', ''); - const tags = this.parseTags(tagsFilter); - triggerList = _.filter(triggerList, trigger => { - return _.every(tags, (tag) => { - return _.find(trigger.tags, {tag: tag.tag, value: tag.value}); - }); - }); - } - - return triggerList; - } - - filterTriggersPost(triggers) { - let triggerList = _.cloneDeep(triggers); - - // Filter acknowledged triggers - if (this.panel.showTriggers === 'unacknowledged') { - triggerList = _.filter(triggerList, trigger => { - return !(trigger.acknowledges && trigger.acknowledges.length); - }); - } else if (this.panel.showTriggers === 'acknowledged') { - triggerList = _.filter(triggerList, trigger => { - return trigger.acknowledges && trigger.acknowledges.length; - }); - } - - // Filter by maintenance status - if (!this.panel.hostsInMaintenance) { - triggerList = _.filter(triggerList, (trigger) => trigger.maintenance === false); - } - - // Filter triggers by severity - triggerList = _.filter(triggerList, trigger => { - return this.panel.triggerSeverity[trigger.priority].show; - }); - - return triggerList; - } - - setMaintenanceStatus(triggers) { - _.each(triggers, (trigger) => { - let maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1'); - trigger.maintenance = maintenance_status; - }); - return triggers; - } - - setAckButtonStatus(triggers, showAckButton) { - _.each(triggers, (trigger) => { - trigger.showAckButton = showAckButton; - }); - return triggers; - } - - addTriggerDataSource(triggers, target) { - _.each(triggers, (trigger) => { - trigger.datasource = target.datasource; - }); - return triggers; - } - - addTriggerHostProxy(triggers, proxies) { - triggers.forEach(trigger => { - if (trigger.hosts && trigger.hosts.length) { - let host = trigger.hosts[0]; - if (host.proxy_hostid !== '0') { - const hostProxy = proxies[host.proxy_hostid]; - host.proxy = hostProxy ? hostProxy.host : ''; - } - } - }); - return triggers; - } - - sortTriggers(triggerList) { - if (this.panel.sortTriggersBy.value === 'priority') { - triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']); - } else { - triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']); - } - return triggerList; - } - - formatTrigger(zabbixTrigger) { - let trigger = _.cloneDeep(zabbixTrigger); - - // Set host and proxy that the trigger belongs - if (trigger.hosts && trigger.hosts.length) { - const host = trigger.hosts[0]; - trigger.host = host.name; - trigger.hostTechName = host.host; - if (host.proxy) { - trigger.proxy = host.proxy; - } - } - - // Set tags if present - if (trigger.tags && trigger.tags.length === 0) { - trigger.tags = null; - } - - // Handle multi-line description - if (trigger.comments) { - trigger.comments = trigger.comments.replace('\n', '
'); - } - - trigger.lastchangeUnix = Number(trigger.lastchange); - return trigger; - } - - parseTags(tagStr) { - if (!tagStr) { - return []; - } - - let tags = _.map(tagStr.split(','), (tag) => tag.trim()); - tags = _.map(tags, (tag) => { - const tagParts = tag.split(':'); - return {tag: tagParts[0].trim(), value: tagParts[1].trim()}; - }); - return tags; - } - - tagsToString(tags) { - return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', '); - } - - addTagFilter(tag, datasource) { - const target = this.panel.targets.find(t => t.datasource === datasource); - console.log(target); - let tagFilter = target.tags.filter; - let targetTags = this.parseTags(tagFilter); - let newTag = {tag: tag.tag, value: tag.value}; - targetTags.push(newTag); - targetTags = _.uniqWith(targetTags, _.isEqual); - let newFilter = this.tagsToString(targetTags); - target.tags.filter = newFilter; - this.refresh(); - } - - removeTagFilter(tag, datasource) { - const target = this.panel.targets.find(t => t.datasource === datasource); - let tagFilter = target.tags.filter; - let targetTags = this.parseTags(tagFilter); - _.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value); - targetTags = _.uniqWith(targetTags, _.isEqual); - let newFilter = this.tagsToString(targetTags); - target.tags.filter = newFilter; - this.refresh(); - } - - getProblemEvents(problem) { - const triggerids = [problem.triggerid]; - const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); - const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); - return this.datasourceSrv.get(problem.datasource) - .then(datasource => { - return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT); - }); - } - - getProblemAlerts(problem) { - if (!problem.lastEvent || problem.lastEvent.length === 0) { - return Promise.resolve([]); - } - const eventids = [problem.lastEvent.eventid]; - return this.datasourceSrv.get(problem.datasource) - .then(datasource => { - return datasource.zabbix.getEventAlerts(eventids); - }); - } - - formatHostName(trigger) { - let host = ""; - if (this.panel.hostField && this.panel.hostTechNameField) { - host = `${trigger.host} (${trigger.hostTechName})`; - } else if (this.panel.hostField || this.panel.hostTechNameField) { - host = this.panel.hostField ? trigger.host : trigger.hostTechName; - } - if (this.panel.hostProxy && trigger.proxy) { - host = `${trigger.proxy}: ${host}`; - } - - return host; - } - - formatHostGroups(trigger) { - let groupNames = ""; - if (this.panel.hostGroups) { - let groups = _.map(trigger.groups, 'name').join(', '); - groupNames += `[ ${groups} ]`; - } - - return groupNames; - } - - isNewTrigger(trigger) { - try { - const highlightIntervalMs = utils.parseInterval(this.panel.highlightNewerThan || PANEL_DEFAULTS.highlightNewerThan); - const durationSec = (Date.now() - trigger.lastchangeUnix * 1000); - return durationSec < highlightIntervalMs; - } catch (e) { - return false; - } - } - - getAlertIconClass(trigger) { - let iconClass = ''; - if (trigger.value === '1' && trigger.priority >= 2) { - iconClass = 'icon-gf-critical'; - } else { - iconClass = 'icon-gf-online'; - } - - if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) { - iconClass += ' zabbix-trigger--blinked'; - } - return iconClass; - } - - getAlertIconClassBySeverity(triggerSeverity) { - let iconClass = 'icon-gf-online'; - if (triggerSeverity.priority >= 2) { - iconClass = 'icon-gf-critical'; - } - return iconClass; - } - - getAlertStateClass(trigger) { - let statusClass = ''; - - if (trigger.value === '1') { - statusClass = 'alert-state-critical'; - } else { - statusClass = 'alert-state-ok'; - } - - if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) { - statusClass += ' zabbix-trigger--blinked'; - } - - return statusClass; - } - - resetResizedColumns() { - this.panel.resizedColumns = []; - this.render(); - } - - acknowledgeTrigger(trigger, message) { - let eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null; - let grafana_user = this.contextSrv.user.name; - let ack_message = grafana_user + ' (Grafana): ' + message; - return this.datasourceSrv.get(trigger.datasource) - .then(datasource => { - const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin; - if (datasource.disableReadOnlyUsersAck && !userIsEditor) { - return Promise.reject({message: 'You have no permissions to acknowledge events.'}); - } - if (eventid) { - return datasource.zabbix.acknowledgeEvent(eventid, ack_message); - } else { - return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'}); - } - }) - .then(this.onRefresh.bind(this)) - .catch((err) => { - this.setPanelError(err); - }); - } - - handlePageSizeChange(pageSize, pageIndex) { - this.panel.pageSize = pageSize; - this.pageIndex = pageIndex; - this.scope.$apply(() => { - this.render(); - }); - } - - handleColumnResize(newResized) { - this.panel.resizedColumns = newResized; - this.scope.$apply(() => { - this.render(); - }); - } - - link(scope, elem, attrs, ctrl) { - let panel = ctrl.panel; - let triggerList = ctrl.triggerList; - - scope.$watchGroup(['ctrl.triggerList'], renderPanel); - ctrl.events.on('render', (renderData) => { - triggerList = renderData || triggerList; - renderPanel(); - }); - - function renderPanel() { - const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000); - const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000); - - const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1)); - const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null; - - const pageSize = panel.pageSize || 10; - const loading = ctrl.loading && (!ctrl.triggerList || !ctrl.triggerList.length); - - let panelOptions = {}; - for (let prop in PANEL_DEFAULTS) { - panelOptions[prop] = ctrl.panel[prop]; - } - const problemsListProps = { - problems: ctrl.triggerList, - panelOptions, - timeRange: { timeFrom, timeTo }, - loading, - pageSize, - fontSize: fontSizeProp, - getProblemEvents: ctrl.getProblemEvents.bind(ctrl), - getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl), - onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl), - onColumnResize: ctrl.handleColumnResize.bind(ctrl), - onProblemAck: (trigger, data) => { - const message = data.message; - return ctrl.acknowledgeTrigger(trigger, message); - }, - onTagClick: (tag, datasource, ctrlKey, shiftKey) => { - if (ctrlKey || shiftKey) { - ctrl.removeTagFilter(tag, datasource); - } else { - ctrl.addTagFilter(tag, datasource); - } - } - }; - - let problemsReactElem; - if (panel.layout === 'list') { - problemsReactElem = React.createElement(AlertList, problemsListProps); - } else { - problemsReactElem = React.createElement(ProblemList, problemsListProps); - } - ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]); - } - } -} - -TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html'; - -function filterTriggers(triggers, triggerFilter) { - if (utils.isRegex(triggerFilter)) { - return _.filter(triggers, function(trigger) { - return utils.buildRegex(triggerFilter).test(trigger.description); - }); - } else { - return _.filter(triggers, function(trigger) { - return trigger.description === triggerFilter; - }); - } -} diff --git a/src/panel-triggers/triggers_panel_ctrl.ts b/src/panel-triggers/triggers_panel_ctrl.ts new file mode 100644 index 0000000..075bfcb --- /dev/null +++ b/src/panel-triggers/triggers_panel_ctrl.ts @@ -0,0 +1,434 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import _ from 'lodash'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { PanelEvents } from '@grafana/data'; +import * as dateMath from 'grafana/app/core/utils/datemath'; +import * as utils from '../datasource-zabbix/utils'; +import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; +import { triggerPanelOptionsTab } from './options_tab'; +import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations'; +import ProblemList from './components/Problems/Problems'; +import AlertList from './components/AlertList/AlertList'; +import { ProblemDTO } from 'datasource-zabbix/types'; + +const PROBLEM_EVENTS_LIMIT = 100; + +export const DEFAULT_TARGET = { + group: {filter: ""}, + host: {filter: ""}, + application: {filter: ""}, + trigger: {filter: ""}, + tags: {filter: ""}, + proxy: {filter: ""}, + showProblems: 'problems', +}; + +export const DEFAULT_SEVERITY = [ + { priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true}, + { priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true}, + { priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true}, + { priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true}, + { priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true}, + { priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true}, +]; + +export const getDefaultSeverity = () => DEFAULT_SEVERITY; + +const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; + +export const PANEL_DEFAULTS = { + schemaVersion: CURRENT_SCHEMA_VERSION, + // Fields + hostField: true, + hostTechNameField: false, + hostProxy: false, + hostGroups: false, + showTags: true, + statusField: true, + statusIcon: false, + severityField: true, + ackField: true, + ageField: false, + descriptionField: true, + descriptionAtNewLine: false, + // Options + sortProblems: 'lastchange', + limit: null, + // View options + layout: 'table', + fontSize: '100%', + pageSize: 10, + problemTimeline: true, + highlightBackground: false, + highlightNewEvents: false, + highlightNewerThan: '1h', + customLastChangeFormat: false, + lastChangeFormat: "", + resizedColumns: [], + // Triggers severity and colors + triggerSeverity: getDefaultSeverity(), + okEventColor: 'rgb(56, 189, 113)', + ackEventColor: 'rgb(56, 219, 156)', + markAckEvents: false, +}; + +const triggerStatusMap = { + '0': 'OK', + '1': 'PROBLEM' +}; + +export class TriggerPanelCtrl extends MetricsPanelCtrl { + scope: any; + useDataFrames: boolean; + triggerStatusMap: any; + defaultTimeFormat: string; + pageIndex: number; + renderData: any[]; + problems: any[]; + contextSrv: any; + static templateUrl: string; + + /** @ngInject */ + constructor($scope, $injector, $timeout) { + super($scope, $injector); + this.scope = $scope; + this.$timeout = $timeout; + + // Tell Grafana do not convert data frames to table or series + this.useDataFrames = true; + + this.triggerStatusMap = triggerStatusMap; + this.defaultTimeFormat = DEFAULT_TIME_FORMAT; + this.pageIndex = 0; + this.range = {}; + this.renderData = []; + + this.panel = migratePanelSchema(this.panel); + _.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS)); + + // this.events.on(PanelEvents.render, this.onRender.bind(this)); + this.events.on('data-frames-received', this.onDataFramesReceived.bind(this)); + this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this)); + this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this)); + } + + onInitEditMode() { + // Update schema version to prevent migration on up-to-date targets + this.panel.schemaVersion = CURRENT_SCHEMA_VERSION; + this.addEditorTab('Options', triggerPanelOptionsTab); + } + + onDataFramesReceived(data: any): Promise { + this.range = this.timeSrv.timeRange(); + let problems = []; + + if (data && data.length) { + for (const dataFrame of data) { + try { + const values = dataFrame.fields[0].values; + if (values.toArray) { + problems.push(values.toArray()); + } else if (values.length > 0) { + // On snapshot mode values is a plain Array, not ArrayVector + problems.push(values); + } + } catch (error) { + console.log(error); + return Promise.reject(error); + } + } + } + + this.loading = false; + problems = _.flatten(problems); + this.problems = problems; + return this.renderProblems(problems); + } + + onDataSnapshotLoad(snapshotData) { + return this.onDataFramesReceived(snapshotData); + } + + reRenderProblems() { + if (this.problems) { + this.renderProblems(this.problems); + } + } + + setPanelError(err, defaultError = "Request Error") { + this.inspector = { error: err }; + this.error = err.message || defaultError; + if (err.data) { + if (err.data.message) { + this.error = err.data.message; + } + if (err.data.error) { + this.error = err.data.error; + } + } + + // this.events.emit(PanelEvents.dataError, err); + console.log('Panel data error:', err); + } + + renderProblems(problems) { + let triggers = _.cloneDeep(problems); + + triggers = _.map(triggers, this.formatTrigger.bind(this)); + triggers = this.filterProblems(triggers); + triggers = this.sortTriggers(triggers); + + this.renderData = triggers; + + return this.$timeout(() => { + return super.render(triggers); + }); + } + + filterProblems(problems) { + let problemsList = _.cloneDeep(problems); + + // Filter acknowledged triggers + if (this.panel.showTriggers === 'unacknowledged') { + problemsList = _.filter(problemsList, trigger => { + return !(trigger.acknowledges && trigger.acknowledges.length); + }); + } else if (this.panel.showTriggers === 'acknowledged') { + problemsList = _.filter(problemsList, trigger => { + return trigger.acknowledges && trigger.acknowledges.length; + }); + } + + // Filter triggers by severity + problemsList = _.filter(problemsList, problem => { + if (problem.severity) { + return this.panel.triggerSeverity[problem.severity].show; + } else { + return this.panel.triggerSeverity[problem.priority].show; + } + }); + + return problemsList; + } + + sortTriggers(triggerList) { + if (this.panel.sortProblems === 'priority') { + triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']); + } else if (this.panel.sortProblems === 'lastchange') { + triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']); + } + return triggerList; + } + + formatTrigger(zabbixTrigger) { + const trigger = _.cloneDeep(zabbixTrigger); + + // Set host and proxy that the trigger belongs + if (trigger.hosts && trigger.hosts.length) { + const host = trigger.hosts[0]; + trigger.host = host.name; + trigger.hostTechName = host.host; + if (host.proxy) { + trigger.proxy = host.proxy; + } + } + + // Set tags if present + if (trigger.tags && trigger.tags.length === 0) { + trigger.tags = null; + } + + // Handle multi-line description + if (trigger.comments) { + trigger.comments = trigger.comments.replace('\n', '
'); + } + + trigger.lastchangeUnix = Number(trigger.lastchange); + return trigger; + } + + parseTags(tagStr) { + if (!tagStr) { + return []; + } + + let tags = _.map(tagStr.split(','), (tag) => tag.trim()); + tags = _.map(tags, (tag) => { + const tagParts = tag.split(':'); + return {tag: tagParts[0].trim(), value: tagParts[1].trim()}; + }); + return tags; + } + + tagsToString(tags) { + return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', '); + } + + addTagFilter(tag, datasource) { + for (const target of this.panel.targets) { + if (target.datasource === datasource || this.panel.datasource === datasource) { + const tagFilter = target.tags.filter; + let targetTags = this.parseTags(tagFilter); + const newTag = {tag: tag.tag, value: tag.value}; + targetTags.push(newTag); + targetTags = _.uniqWith(targetTags, _.isEqual); + const newFilter = this.tagsToString(targetTags); + target.tags.filter = newFilter; + } + } + this.refresh(); + } + + removeTagFilter(tag, datasource) { + const matchTag = t => t.tag === tag.tag && t.value === tag.value; + for (const target of this.panel.targets) { + if (target.datasource === datasource || this.panel.datasource === datasource) { + const tagFilter = target.tags.filter; + let targetTags = this.parseTags(tagFilter); + _.remove(targetTags, matchTag); + targetTags = _.uniqWith(targetTags, _.isEqual); + const newFilter = this.tagsToString(targetTags); + target.tags.filter = newFilter; + } + } + this.refresh(); + } + + getProblemEvents(problem) { + const triggerids = [problem.triggerid]; + const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); + const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); + return getDataSourceSrv().get(problem.datasource) + .then((datasource: any) => { + return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT); + }); + } + + getProblemAlerts(problem: ProblemDTO) { + if (!problem.eventid) { + return Promise.resolve([]); + } + const eventids = [problem.eventid]; + return getDataSourceSrv().get(problem.datasource) + .then((datasource: any) => { + return datasource.zabbix.getEventAlerts(eventids); + }); + } + + getAlertIconClassBySeverity(triggerSeverity) { + let iconClass = 'icon-gf-online'; + if (triggerSeverity.priority >= 2) { + iconClass = 'icon-gf-critical'; + } + return iconClass; + } + + resetResizedColumns() { + this.panel.resizedColumns = []; + this.render(); + } + + acknowledgeProblem(problem: ProblemDTO, message, action, severity) { + const eventid = problem.eventid; + const grafana_user = this.contextSrv.user.name; + const ack_message = grafana_user + ' (Grafana): ' + message; + return getDataSourceSrv().get(problem.datasource) + .then((datasource: any) => { + const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin; + if (datasource.disableReadOnlyUsersAck && !userIsEditor) { + return Promise.reject({message: 'You have no permissions to acknowledge events.'}); + } + if (eventid) { + return datasource.zabbix.acknowledgeEvent(eventid, ack_message, action, severity); + } else { + return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'}); + } + }) + .then(this.refresh.bind(this)) + .catch((err) => { + this.setPanelError(err); + return Promise.reject(err); + }); + } + + handlePageSizeChange(pageSize, pageIndex) { + this.panel.pageSize = pageSize; + this.pageIndex = pageIndex; + this.scope.$apply(() => { + this.render(); + }); + } + + handleColumnResize(newResized) { + this.panel.resizedColumns = newResized; + this.scope.$apply(() => { + this.render(); + }); + } + + link(scope, elem, attrs, ctrl) { + const panel = ctrl.panel; + + ctrl.events.on(PanelEvents.render, (renderData) => { + renderData = renderData || this.renderData; + renderPanel(renderData); + }); + + function renderPanel(problems) { + const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000); + const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000); + + const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1), 10); + const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null; + + const pageSize = panel.pageSize || 10; + const loading = ctrl.loading && (!problems || !problems.length); + + const panelOptions = {}; + for (const prop in PANEL_DEFAULTS) { + panelOptions[prop] = ctrl.panel[prop]; + } + const problemsListProps = { + problems, + panelOptions, + timeRange: { timeFrom, timeTo }, + loading, + pageSize, + fontSize: fontSizeProp, + panelId: ctrl.panel.id, + getProblemEvents: ctrl.getProblemEvents.bind(ctrl), + getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl), + onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl), + onColumnResize: ctrl.handleColumnResize.bind(ctrl), + onProblemAck: (trigger, data) => { + const { message, action, severity } = data; + return ctrl.acknowledgeProblem(trigger, message, action, severity); + }, + onTagClick: (tag, datasource, ctrlKey, shiftKey) => { + if (ctrlKey || shiftKey) { + ctrl.removeTagFilter(tag, datasource); + } else { + ctrl.addTagFilter(tag, datasource); + } + } + }; + + let problemsReactElem; + if (panel.layout === 'list') { + problemsReactElem = React.createElement(AlertList, problemsListProps); + } else { + problemsReactElem = React.createElement(ProblemList, problemsListProps); + } + + const panelContainerElem = elem.find('.panel-content'); + if (panelContainerElem && panelContainerElem.length) { + ReactDOM.render(problemsReactElem, panelContainerElem[0]); + } else { + ReactDOM.render(problemsReactElem, elem[0]); + } + } + } +} + +TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html'; diff --git a/src/panel-triggers/triggers_tab.js b/src/panel-triggers/triggers_tab.js deleted file mode 100644 index 9b3032b..0000000 --- a/src/panel-triggers/triggers_tab.js +++ /dev/null @@ -1,131 +0,0 @@ -import _ from 'lodash'; -import * as utils from '../datasource-zabbix/utils'; -import { getDefaultTarget } from './triggers_panel_ctrl'; - -class TriggersTabCtrl { - - /** @ngInject */ - constructor($scope, $rootScope, uiSegmentSrv, templateSrv) { - $scope.editor = this; - this.panelCtrl = $scope.ctrl; - this.panel = this.panelCtrl.panel; - this.templateSrv = templateSrv; - this.datasources = {}; - - // Load scope defaults - var scopeDefaults = { - getGroupNames: {}, - getHostNames: {}, - getApplicationNames: {}, - getProxyNames: {}, - oldTarget: _.cloneDeep(this.panel.targets) - }; - _.defaultsDeep(this, scopeDefaults); - this.selectedDatasources = this.getSelectedDatasources(); - - this.initDatasources(); - this.panelCtrl.refresh(); - } - - initDatasources() { - return this.panelCtrl.initDatasources() - .then((datasources) => { - _.each(datasources, (datasource) => { - this.datasources[datasource.name] = datasource; - this.bindSuggestionFunctions(datasource); - }); - }); - } - - bindSuggestionFunctions(datasource) { - // Map functions for bs-typeahead - let ds = datasource.name; - this.getGroupNames[ds] = _.bind(this.suggestGroups, this, datasource); - this.getHostNames[ds] = _.bind(this.suggestHosts, this, datasource); - this.getApplicationNames[ds] = _.bind(this.suggestApps, this, datasource); - this.getProxyNames[ds] = _.bind(this.suggestProxies, this, datasource); - } - - getSelectedDatasources() { - return _.compact(this.panel.targets.map(target => target.datasource)); - } - - suggestGroups(datasource, query, callback) { - return datasource.zabbix.getAllGroups() - .then(groups => { - return _.map(groups, 'name'); - }) - .then(callback); - } - - suggestHosts(datasource, query, callback) { - const target = this.panel.targets.find(t => t.datasource === datasource.name); - let groupFilter = datasource.replaceTemplateVars(target.group.filter); - return datasource.zabbix.getAllHosts(groupFilter) - .then(hosts => { - return _.map(hosts, 'name'); - }) - .then(callback); - } - - suggestApps(datasource, query, callback) { - const target = this.panel.targets.find(t => t.datasource === datasource.name); - let groupFilter = datasource.replaceTemplateVars(target.group.filter); - let hostFilter = datasource.replaceTemplateVars(target.host.filter); - return datasource.zabbix.getAllApps(groupFilter, hostFilter) - .then(apps => { - return _.map(apps, 'name'); - }) - .then(callback); - } - - suggestProxies(datasource, query, callback) { - return datasource.zabbix.getProxies() - .then(proxies => _.map(proxies, 'host')) - .then(callback); - } - - datasourcesChanged() { - const newTargets = []; - _.each(this.selectedDatasources, (ds) => { - const dsTarget = this.panel.targets.find((target => target.datasource === ds)); - if (dsTarget) { - newTargets.push(dsTarget); - } else { - const newTarget = getDefaultTarget(this.panel.targets); - newTarget.datasource = ds; - newTargets.push(newTarget); - } - this.panel.targets = newTargets; - }); - this.parseTarget(); - } - - parseTarget() { - this.initDatasources() - .then(() => { - var newTarget = _.cloneDeep(this.panel.targets); - if (!_.isEqual(this.oldTarget, newTarget)) { - this.oldTarget = newTarget; - this.panelCtrl.refresh(); - } - }); - } - - isRegex(str) { - return utils.isRegex(str); - } - - isVariable(str) { - return utils.isTemplateVariable(str, this.templateSrv.variables); - } -} - -export function triggerPanelTriggersTab() { - return { - restrict: 'E', - scope: true, - templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/triggers_tab.html', - controller: TriggersTabCtrl, - }; -} diff --git a/src/panel-triggers/types.ts b/src/panel-triggers/types.ts index e0ab9a0..9f2b03f 100644 --- a/src/panel-triggers/types.ts +++ b/src/panel-triggers/types.ts @@ -11,6 +11,7 @@ export interface ProblemsPanelOptions { statusField?: boolean; statusIcon?: boolean; severityField?: boolean; + ackField?: boolean; ageField?: boolean; descriptionField?: boolean; descriptionAtNewLine?: boolean; @@ -140,6 +141,7 @@ export interface ZBXEvent { object?: string; objectid?: string; acknowledged?: string; + severity?: string; hosts?: ZBXHost[]; acknowledges?: ZBXAcknowledge[]; } diff --git a/src/panel-triggers/utils.ts b/src/panel-triggers/utils.ts index 6976c3a..7ccda39 100644 --- a/src/panel-triggers/utils.ts +++ b/src/panel-triggers/utils.ts @@ -2,12 +2,12 @@ import _ from 'lodash'; import moment from 'moment'; import { DataQuery } from '@grafana/data'; import * as utils from '../datasource-zabbix/utils'; -import { ZBXTrigger } from './types'; +import { ProblemDTO } from 'datasource-zabbix/types'; -export function isNewProblem(problem: ZBXTrigger, highlightNewerThan: string): boolean { +export function isNewProblem(problem: ProblemDTO, highlightNewerThan: string): boolean { try { const highlightIntervalMs = utils.parseInterval(highlightNewerThan); - const durationSec = (Date.now() - problem.lastchangeUnix * 1000); + const durationSec = (Date.now() - problem.timestamp * 1000); return durationSec < highlightIntervalMs; } catch (e) { return false; @@ -32,3 +32,74 @@ export const getNextRefIdChar = (queries: DataQuery[]): string => { }); }); }; + +export type UrlQueryMap = Record; + +export function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { + path += '?' + toUrlParams(query); + } + return path; +} + +function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) { + return encodeURIComponent(val) + .replace(/%25/gi, '%2525') // Double-encode % symbol to make it properly decoded in Explore + .replace(/%40/gi, '@') + .replace(/%3A/gi, ':') + .replace(/%24/g, '$') + .replace(/%2C/gi, ',') + .replace(/%3B/gi, ';') + .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); +} + +function toUrlParams(a: any) { + const s: any[] = []; + const rbracket = /\[\]$/; + + const isArray = (obj: any) => { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + + const add = (k: string, v: any) => { + v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; + if (typeof v !== 'boolean') { + s[s.length] = encodeURIComponentAsAngularJS(k, true) + '=' + encodeURIComponentAsAngularJS(v, true); + } else { + s[s.length] = encodeURIComponentAsAngularJS(k, true); + } + }; + + const buildParams = (prefix: string, obj: any) => { + let i, len, key; + + if (prefix) { + if (isArray(obj)) { + for (i = 0, len = obj.length; i < len; i++) { + if (rbracket.test(prefix)) { + add(prefix, obj[i]); + } else { + buildParams(prefix, obj[i]); + } + } + } else if (obj && String(obj) === '[object Object]') { + for (key in obj) { + buildParams(prefix + '[' + key + ']', obj[key]); + } + } else { + add(prefix, obj); + } + } else if (isArray(obj)) { + for (i = 0, len = obj.length; i < len; i++) { + add(obj[i].name, obj[i].value); + } + } else { + for (key in obj) { + buildParams(key, obj[key]); + } + } + return s; + }; + + return buildParams('', a).join('&'); +} diff --git a/src/plugin.json b/src/plugin.json index 098406c..8a5df1c 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -11,8 +11,8 @@ }, "keywords": ["zabbix"], "logos": { - "small": "img/zabbix_app_logo.svg", - "large": "img/zabbix_app_logo.svg" + "small": "img/icn-zabbix-app.svg", + "large": "img/icn-zabbix-app.svg" }, "links": [ {"name": "GitHub", "url": "https://github.com/alexanderzobnin/grafana-zabbix"}, @@ -27,17 +27,17 @@ {"name": "Triggers", "path": "img/screenshot-triggers.png"} ], "version": "4.0.0-alpha", - "updated": "2020-01-14" + "updated": "2020-05-28" }, "includes": [ { "type": "datasource", - "name": "Zabbix Datasource" + "name": "Zabbix data source" }, { "type": "panel", - "name": "Triggers Panel" + "name": "Problems panel" } ], diff --git a/src/sass/_panel-problems.scss b/src/sass/_panel-problems.scss index 87ba864..d30368a 100644 --- a/src/sass/_panel-problems.scss +++ b/src/sass/_panel-problems.scss @@ -7,6 +7,11 @@ i { width: 1rem; } + + &.fired { + color: $problem-statusbar-fired; + text-shadow: 0px 0px 10px rgba($problem-statusbar-fired, 0.5); + } } // styles @@ -246,10 +251,30 @@ flex-direction: column; } - .description-label { - font-weight: 500; - font-style: italic; - color: $text-muted; + .problem-description-row { + + .problem-description { + position: relative; + height: 4.5rem; + overflow: hidden; + + &:after { + content: ""; + text-align: right; + position: absolute; + bottom: 0; + right: 0; + width: 70%; + height: 1.5rem; + background: linear-gradient(to right, rgba($problem-details-background, 0), rgba($problem-details-background, 1) 50%); + } + } + + .description-label { + font-weight: 500; + font-style: italic; + color: $text-muted; + } } .problem-age { @@ -314,24 +339,8 @@ margin-left: 1.6rem; } - .problem-action-button { - &.btn { - width: 3rem; - height: 2rem; - - background-image: none; - background-color: $action-button-color; - border: 1px solid darken($action-button-color, 6%); - border-radius: 1px; - - span { - color: $action-button-text-color; - } - - &:hover { - background-color: darken($action-button-color, 4%); - } - } + .problem-actions-left { + margin-right: 1.6rem; } .problem-details-middle { @@ -546,9 +555,19 @@ } .zbx-ack-modal { - .gf-form-input.zbx-ack-error { + .zbx-ack-error { border-color: $btn-danger-bg; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px $btn-danger-bg; + // box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px $btn-danger-bg; + outline-offset: 2px; + box-shadow: 0 0 0 2px #141619, 0 0 0px 4px $btn-danger-bg; + } + + .ack-request-error { + padding-top: 1.2rem; + } + + .ack-error-message { + color: $error-text-color; } .gf-form .gf-form-hint { @@ -558,7 +577,6 @@ &.ack-error-message { float: left; - color: $error-text-color; } } } diff --git a/src/sass/grafana-zabbix.scss b/src/sass/grafana-zabbix.scss index 708c9fa..58c9da1 100644 --- a/src/sass/grafana-zabbix.scss +++ b/src/sass/grafana-zabbix.scss @@ -1,5 +1,5 @@ // DEPENDENCIES -@import '../../node_modules/react-table/react-table.css'; +@import '../../node_modules/react-table-6/react-table.css'; @import 'variables'; @import 'panel-triggers'; diff --git a/src/test-setup/jest-setup.js b/src/test-setup/jest-setup.js index 6c1d766..689b961 100644 --- a/src/test-setup/jest-setup.js +++ b/src/test-setup/jest-setup.js @@ -2,7 +2,9 @@ /* globals global: false */ import { JSDOM } from 'jsdom'; -import { PanelCtrl } from './panelStub'; +import { PanelCtrl, MetricsPanelCtrl } from './panelStub'; + +console.log = () => {}; // Mock Grafana modules that are not available outside of the core project // Required for loading module.js @@ -26,17 +28,34 @@ jest.mock('grafana/app/features/dashboard/dashboard_srv', () => { return {}; }, {virtual: true}); +jest.mock('@grafana/runtime', () => { + return { + getBackendSrv: () => ({ + datasourceRequest: jest.fn().mockResolvedValue(), + }), + }; +}, {virtual: true}); + jest.mock('grafana/app/core/core_module', () => { return { directive: function() {}, }; }, {virtual: true}); -let mockPanelCtrl = PanelCtrl; +jest.mock('grafana/app/core/core', () => ({ + contextSrv: {}, +}), {virtual: true}); + +const mockPanelCtrl = PanelCtrl; +const mockMetricsPanelCtrl = MetricsPanelCtrl; + jest.mock('grafana/app/plugins/sdk', () => { return { QueryCtrl: null, - PanelCtrl: mockPanelCtrl + PanelCtrl: mockPanelCtrl, + loadPluginCss: () => {}, + PanelCtrl: mockPanelCtrl, + MetricsPanelCtrl: mockMetricsPanelCtrl, }; }, {virtual: true}); @@ -92,3 +111,7 @@ let dom = new JSDOM(''); global.window = dom.window; global.document = global.window.document; global.Node = window.Node; + +// Mock Canvas.getContext(), fixes +// Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package) +window.HTMLCanvasElement.prototype.getContext = () => {}; diff --git a/src/test-setup/panelStub.js b/src/test-setup/panelStub.js deleted file mode 100644 index 9c66080..0000000 --- a/src/test-setup/panelStub.js +++ /dev/null @@ -1,99 +0,0 @@ -// JSHint options -/* jshint ignore:start */ - -export class PanelCtrl { - constructor($scope, $injector) { - this.$injector = $injector; - this.$scope = $scope; - this.panel = $scope.panel; - this.timing = {}; - this.events = { - on: () => {}, - emit: () => {} - }; - } - - init() { - } - - renderingCompleted() { - } - - refresh() { - } - - publishAppEvent(evtName, evt) { - } - - changeView(fullscreen, edit) { - } - - viewPanel() { - this.changeView(true, false); - } - - editPanel() { - this.changeView(true, true); - } - - exitFullscreen() { - this.changeView(false, false); - } - - initEditMode() { - } - - changeTab(newIndex) { - } - - addEditorTab(title, directiveFn, index) { - } - - getMenu() { - return []; - } - - getExtendedMenu() { - return []; - } - - otherPanelInFullscreenMode() { - return false; - } - - calculatePanelHeight() { - } - - render(payload) { - } - - toggleEditorHelp(index) { - } - - duplicate() { - } - - updateColumnSpan(span) { - } - - removePanel() { - } - - editPanelJson() { - } - - replacePanel(newPanel, oldPanel) { - } - - sharePanel() { - } - - getInfoMode() { - } - - getInfoContent(options) { - } - - openInspector() { - } -} diff --git a/src/test-setup/panelStub.ts b/src/test-setup/panelStub.ts new file mode 100644 index 0000000..667e96f --- /dev/null +++ b/src/test-setup/panelStub.ts @@ -0,0 +1,162 @@ +import { PanelEvents } from '@grafana/data'; + +export class PanelCtrl { + panel: any; + error: any; + dashboard: any; + pluginName: string; + pluginId: string; + editorTabs: any; + $scope: any; + $injector: any; + $location: any; + $timeout: any; + editModeInitiated: boolean; + height: number; + width: number; + containerHeight: any; + events: any; + loading: boolean; + timing: any; + + constructor($scope, $injector) { + this.$injector = $injector; + this.$scope = $scope; + this.panel = $scope.panel; + this.timing = {}; + this.events = { + on: () => {}, + emit: () => {} + }; + } + + init() { + } + + renderingCompleted() { + } + + refresh() { + } + + publishAppEvent(evtName, evt) { + } + + changeView(fullscreen, edit) { + } + + viewPanel() { + this.changeView(true, false); + } + + editPanel() { + this.changeView(true, true); + } + + exitFullscreen() { + this.changeView(false, false); + } + + initEditMode() { + } + + changeTab(newIndex) { + } + + addEditorTab(title, directiveFn, index) { + } + + getMenu() { + return []; + } + + getExtendedMenu() { + return []; + } + + otherPanelInFullscreenMode() { + return false; + } + + calculatePanelHeight() { + } + + render(payload) { + } + + toggleEditorHelp(index) { + } + + duplicate() { + } + + updateColumnSpan(span) { + } + + removePanel() { + } + + editPanelJson() { + } + + replacePanel(newPanel, oldPanel) { + } + + sharePanel() { + } + + getInfoMode() { + } + + getInfoContent(options) { + } + + openInspector() { + } +} + +export class MetricsPanelCtrl extends PanelCtrl { + scope: any; + datasource: any; + $timeout: any; + contextSrv: any; + datasourceSrv: any; + timeSrv: any; + templateSrv: any; + range: any; + interval: any; + intervalMs: any; + resolution: any; + timeInfo?: string; + skipDataOnInit: boolean; + dataList: any[]; + querySubscription?: any; + useDataFrames = false; + + constructor($scope, $injector) { + super($scope, $injector); + + this.events.on(PanelEvents.refresh, this.onMetricsPanelRefresh.bind(this)); + + this.timeSrv = { + timeRange: () => {}, + }; + } + + onInitMetricsPanelEditMode() {} + onMetricsPanelRefresh() {} + setTimeQueryStart() {} + setTimeQueryEnd() {} + updateTimeRange() {} + calculateInterval() {} + applyPanelTimeOverrides() {} + issueQueries(datasource) {} + handleQueryResult(result) {} + handleDataStream(stream) {} + setDatasource(datasource) {} + getAdditionalMenuItems() {} + explore() {} + addQuery(target) {} + removeQuery(target) {} + moveQuery(target, direction) {} +} diff --git a/tsconfig.json b/tsconfig.json index 999b8b9..ee6b64c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "target": "es5", - "lib": [ - "es6", - "dom" - ], + "lib": [ "es6", "dom", "es2017" ], "rootDir": "./src", "jsx": "react", "module": "esnext", @@ -21,6 +18,7 @@ "noImplicitUseStrict": false, "noImplicitAny": false, "noUnusedLocals": false, - "baseUrl": "./src" + "baseUrl": "./src", + "strictFunctionTypes": false } } diff --git a/webpack/webpack.base.conf.js b/webpack/webpack.base.conf.js index 9526fd9..c845687 100644 --- a/webpack/webpack.base.conf.js +++ b/webpack/webpack.base.conf.js @@ -15,8 +15,8 @@ module.exports = { target: 'node', context: resolve('src'), entry: { - './module': './module.js', - 'components/config': './components/config.js', + 'module': './module.js', + 'app_config_ctrl/config': './app_config_ctrl/config.js', 'datasource-zabbix/module': './datasource-zabbix/module.ts', 'panel-triggers/module': './panel-triggers/module.js', }, @@ -42,6 +42,7 @@ module.exports = { new CopyWebpackPlugin([ { from: '**/plugin.json' }, { from: '**/*.html' }, + { from: '**/*.md' }, { from: 'dashboards/*' }, { from: '../README.md' }, { from: '**/img/*' }, diff --git a/yarn.lock b/yarn.lock index f5bb95c..9d1e9f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,10 +33,10 @@ dependencies: "@antv/util" "~1.3.1" -"@antv/g2@3.5.9": - version "3.5.9" - resolved "https://registry.yarnpkg.com/@antv/g2/-/g2-3.5.9.tgz#7037263d50e93ac7fd30a4706c1b9a54f883ef29" - integrity sha512-AS0Exn9Khhx4Xp8JViv37/wjJbiC9zVY02hIdvUeTx4SaKC0nhE0euPfmthen1cQw7nVlGLYEGoav/qxpLAhiw== +"@antv/g2@3.5.15": + version "3.5.15" + resolved "https://registry.yarnpkg.com/@antv/g2/-/g2-3.5.15.tgz#5951808f88210f4a45ca1acb38fb25a743b4a578" + integrity sha512-gWN28V/BRHrCe6O12WcJ7ji9UE8XETSQ146ur4zMu5I50ZO7kxc/3s038N0iyuJh3Em9nlrTjfhqjlysogrX7g== dependencies: "@antv/adjust" "~0.1.0" "@antv/attr" "~0.1.2" @@ -66,9 +66,9 @@ integrity sha512-oOWcVNlpELIKi9x+Mm1Vwbz8pXfkbJKykoCIOJ/dNK79hSIANbpXJ5d3Rra9/wZqK6MC961B7sybFhPlLraT3Q== "@antv/scale@~0.1.1": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@antv/scale/-/scale-0.1.3.tgz#4876e6140cb7dcda190e7fe2e780882dcac6b09d" - integrity sha512-oknlOg4OUqIh8LygrfQttx+OAnNJm2fQ81si4g8aby1WJJwj/TU1gCr+J3loIpKBtBK4VpP/OzTTqg1Ym67SOQ== + version "0.1.5" + resolved "https://registry.yarnpkg.com/@antv/scale/-/scale-0.1.5.tgz#243266e8b9047cf64b2fdfc40f9834cf0846496e" + integrity sha512-7RAu4iH5+Hk21h6+aBMiDTfmLf4IibK2SWjx/+E4f4AXRpqucO+8u7IbZdFkakAWxvqhJtN3oePJuTKqOMcmlg== dependencies: "@antv/util" "~1.3.1" fecha "~2.3.3" @@ -80,7 +80,7 @@ dependencies: "@antv/gl-matrix" "^2.7.1" -"@babel/code-frame@7.5.5", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== @@ -918,26 +918,40 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.4.0", "@babel/template@^7.6.0", "@babel/template@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8" - integrity sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ== +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6": + version "7.8.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d" + integrity sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.3" - "@babel/types" "^7.8.3" + regenerator-runtime "^0.13.4" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.3", "@babel/traverse@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.3.tgz#a826215b011c9b4f73f3a893afbc05151358bf9a" - integrity sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg== +"@babel/runtime@^7.5.5": + version "7.7.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf" + integrity sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.3" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.8.3" - "@babel/types" "^7.8.3" + regenerator-runtime "^0.13.2" + +"@babel/template@^7.4.0", "@babel/template@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" + integrity sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.7.4" + "@babel/types" "^7.7.4" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.4": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558" + integrity sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.7.4" + "@babel/helper-function-name" "^7.7.4" + "@babel/helper-split-export-declaration" "^7.7.4" + "@babel/parser" "^7.7.4" + "@babel/types" "^7.7.4" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" @@ -968,23 +982,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@csstools/convert-colors@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" - integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== - -"@emotion/babel-utils@^0.6.4": - version "0.6.10" - resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" - integrity sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow== - dependencies: - "@emotion/hash" "^0.6.6" - "@emotion/memoize" "^0.6.6" - "@emotion/serialize" "^0.9.1" - convert-source-map "^1.5.1" - find-root "^1.1.0" - source-map "^0.7.2" - "@emotion/cache@^10.0.27": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.27.tgz#7895db204e2c1a991ae33d51262a3a44f6737303" @@ -995,6 +992,16 @@ "@emotion/utils" "0.11.3" "@emotion/weak-memoize" "0.2.5" +"@emotion/cache@^10.0.9": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/core@^10.0.27": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.27.tgz#7c3f78be681ab2273f3bf11ca3e2edc4a9dd1fdc" @@ -1007,7 +1014,19 @@ "@emotion/sheet" "0.9.4" "@emotion/utils" "0.11.3" -"@emotion/css@^10.0.27": +"@emotion/core@^10.0.9": + version "10.0.28" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.28.tgz#bb65af7262a234593a9e952c041d0f1c9b9bef3d" + integrity sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + +"@emotion/css@^10.0.27", "@emotion/css@^10.0.9": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== @@ -1021,21 +1040,11 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.4.tgz#f14932887422c9056b15a8d222a9074a7dfa2831" integrity sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A== -"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" - integrity sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ== - "@emotion/memoize@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== -"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": - version "0.6.6" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" - integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== - "@emotion/serialize@^0.11.15": version "0.11.15" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.15.tgz#9a0f5873fb458d87d4f23e034413c12ed60a705a" @@ -1047,16 +1056,6 @@ "@emotion/utils" "0.11.3" csstype "^2.5.7" -"@emotion/serialize@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" - integrity sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ== - dependencies: - "@emotion/hash" "^0.6.6" - "@emotion/memoize" "^0.6.6" - "@emotion/unitless" "^0.6.7" - "@emotion/utils" "^0.8.2" - "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" @@ -1067,53 +1066,37 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/stylis@^0.7.0": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" - integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== - "@emotion/unitless@0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== -"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": - version "0.6.7" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" - integrity sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg== - "@emotion/utils@0.11.3": version "0.11.3" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== -"@emotion/utils@^0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" - integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== - "@emotion/weak-memoize@0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@grafana/data@6.6.0-pre-18fda3b8b4", "@grafana/data@canary": - version "6.6.0-pre-18fda3b8b4" - resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.6.0-pre-18fda3b8b4.tgz#ea281686667216e460f82a62d841a36d1e964942" - integrity sha512-pSowJt1fz88/iuDzcqt0oA9lQntzmbYK9Oqy7bfQino3xAr2Nu9x4Xv8ueUQ9p4Gqe3fTs0UUwioHoarOGoLAQ== - -"@grafana/data@6.6.0-pre-d9e9843a10", "@grafana/data@^6.6.0-pre-18fda3b8b4": - version "6.6.0-pre-d9e9843a10" - resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.6.0-pre-d9e9843a10.tgz#c81e6a30d01dfc4d6fb94dd9f851e3e77503372f" - integrity sha512-uS4sib0jNGD6Xa/Zrn3jvdD9B5cwoGd7coDHfdqOw+ynzbSjUglhj+2cQJ9AUerIqq03j6inXJ0vrkQiEo3g2g== - -"@grafana/runtime@canary": - version "6.6.0-pre-18fda3b8b4" - resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-6.6.0-pre-18fda3b8b4.tgz#3889a6553843d0732563d109df546eeb64e739c1" - integrity sha512-aV/zjaO7k3R9Db+WsUAYfTBqJfRkPSAPlD6k2w3PrGJmi6iX0sHxq6LePvGCqh2g5oLdkSPErAGoV8m0YeoyrQ== +"@grafana/data@6.7.3", "@grafana/data@^6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.7.3.tgz#e9e1bade8c2d35c131df3f3e4d652e5fd6140c23" + integrity sha512-5B8bNfSYFVw49It2/nTuw3NBM8jO/BM4bRT5wqVBbKQDOt94GsUTcsI0oVKl/VCQAwT+rlZdaF5/XTme4DiPhg== dependencies: - "@grafana/data" "6.6.0-pre-18fda3b8b4" - "@grafana/ui" "6.6.0-pre-18fda3b8b4" + apache-arrow "0.15.1" + lodash "4.17.15" + rxjs "6.5.4" + +"@grafana/runtime@^6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-6.7.3.tgz#998b35aaf2ab618bbf19358dde15b455d9227b20" + integrity sha512-6FXusFDrblFAJGAhCscXJxfqMqqBX4A0xHHpaD/rwNTqm6KOFxivQgd2Bf6Cr9W6ws5dNtHPaOC77xsDQIpIMg== + dependencies: + "@grafana/data" "6.7.3" + "@grafana/ui" "6.7.3" systemjs "0.20.19" systemjs-plugin-css "0.1.37" @@ -1139,100 +1122,30 @@ tiny-invariant "^1.0.1" tiny-warning "^0.0.3" -"@grafana/toolkit@canary": - version "6.6.0-pre-18fda3b8b4" - resolved "https://registry.yarnpkg.com/@grafana/toolkit/-/toolkit-6.6.0-pre-18fda3b8b4.tgz#e43b7df42e07ceb67ae234dd8793080a8cd77519" - integrity sha512-CxZGyYiSB5Q2WIe5eLZ0pDGTaHDZ1NCxD6x02snmfRc2HgjAT0A4Tqn1v567Iq8biuVGVH+jS0iv7/FeZaBBKQ== - dependencies: - "@babel/core" "7.6.4" - "@babel/preset-env" "7.6.3" - "@grafana/data" "^6.6.0-pre-18fda3b8b4" - "@grafana/ui" "^6.6.0-pre-18fda3b8b4" - "@types/command-exists" "^1.2.0" - "@types/execa" "^0.9.0" - "@types/expect-puppeteer" "3.3.1" - "@types/inquirer" "^6.0.3" - "@types/jest" "24.0.13" - "@types/jest-cli" "^23.6.0" - "@types/node" "^12.0.4" - "@types/prettier" "^1.16.4" - "@types/puppeteer-core" "1.9.0" - "@types/react-dev-utils" "^9.0.1" - "@types/rimraf" "^2.0.3" - "@types/semver" "^6.0.0" - "@types/tmp" "^0.1.0" - "@types/webpack" "4.4.34" - axios "0.19.0" - babel-jest "24.8.0" - babel-loader "8.0.6" - babel-plugin-angularjs-annotate "0.10.0" - chalk "^2.4.2" - command-exists "^1.2.8" - commander "^2.20.0" - concurrently "4.1.0" - copy-webpack-plugin "5.0.3" - css-loader "^3.0.0" - execa "^1.0.0" - expect-puppeteer "4.1.1" - file-loader "^4.0.0" - globby "^10.0.1" - html-loader "0.5.5" - html-webpack-plugin "^3.2.0" - inquirer "^6.3.1" - jest "24.8.0" - jest-cli "^24.8.0" - jest-coverage-badges "^1.1.2" - jest-junit "^6.4.0" - less "^3.10.3" - less-loader "^5.0.0" - lodash "4.17.15" - md5-file "^4.0.0" - mini-css-extract-plugin "^0.7.0" - node-sass "^4.12.0" - optimize-css-assets-webpack-plugin "^5.0.3" - ora "^3.4.0" - pixelmatch "^5.0.2" - pngjs "^3.4.0" - postcss-flexbugs-fixes "4.1.0" - postcss-loader "3.0.0" - postcss-preset-env "6.6.0" - prettier "^1.19.1" - puppeteer-core "1.18.1" - react-dev-utils "^9.0.1" - replace-in-file "^4.1.0" - replace-in-file-webpack-plugin "^1.0.6" - rimraf "^3.0.0" - sass-loader "7.1.0" - semver "^6.1.1" - simple-git "^1.112.0" - style-loader "^0.23.1" - terser-webpack-plugin "^1.3.0" - ts-jest "24.1.0" - ts-loader "6.2.1" - ts-node "8.5.0" - tslib "1.10.0" - tslint "5.20.1" - tslint-config-prettier "^1.18.0" - typescript "3.7.2" - url-loader "^2.0.1" - webpack "4.35.0" +"@grafana/tsconfig@^1.0.0-rc1": + version "1.0.0-rc1" + resolved "https://registry.yarnpkg.com/@grafana/tsconfig/-/tsconfig-1.0.0-rc1.tgz#d07ea16755a50cae21000113f30546b61647a200" + integrity sha512-nucKPGyzlSKYSiJk5RA8GzMdVWhdYNdF+Hh65AXxjD9PlY69JKr5wANj8bVdQboag6dgg0BFKqgKPyY+YtV4Iw== -"@grafana/ui@6.6.0-pre-18fda3b8b4", "@grafana/ui@canary": - version "6.6.0-pre-18fda3b8b4" - resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.6.0-pre-18fda3b8b4.tgz#d6e4780b040c0087655e65be832e9f46546808a0" - integrity sha512-CxR72UO0GXBbxR0j40AAxRm2R2OhgyV959r4SDbU0EpulShHsmle/Oz0btXbG+gIMaNklUevOhDWdecLtG1pvA== +"@grafana/ui@6.7.3", "@grafana/ui@^6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.7.3.tgz#509d3a252ea0942f3850821cb028669366656044" + integrity sha512-b4Ltk/GlBUa4d/ahCcxG4NAbIDjqLknEcynNaFI3kOj0DzVdlC+AGrv+5f/pZZ+bEr/ZKUdBJMeH0sZIOP+dOg== dependencies: - "@grafana/data" "6.6.0-pre-18fda3b8b4" + "@emotion/core" "^10.0.27" + "@grafana/data" "6.7.3" "@grafana/slate-react" "0.22.9-grafana" - "@torkelo/react-select" "2.1.1" + "@grafana/tsconfig" "^1.0.0-rc1" + "@torkelo/react-select" "3.0.8" "@types/react-color" "2.17.0" - "@types/react-select" "2.0.15" + "@types/react-select" "3.0.8" "@types/react-table" "7.0.2" "@types/slate" "0.47.1" "@types/slate-react" "0.22.5" bizcharts "^3.5.5" classnames "2.2.6" d3 "5.15.0" + emotion "10.0.27" immutable "3.8.2" jquery "3.4.1" lodash "4.17.15" @@ -1240,54 +1153,19 @@ papaparse "4.6.3" rc-cascader "0.17.5" rc-drawer "3.0.2" + rc-slider "8.7.1" rc-time-picker "^3.7.2" react "16.12.0" - react-calendar "2.18.1" + react-calendar "2.19.2" react-color "2.17.0" react-custom-scrollbars "4.2.1" react-dom "16.12.0" react-highlight-words "0.11.0" + react-hook-form "4.5.3" react-popper "1.3.3" react-storybook-addon-props-combinations "1.1.0" react-table "7.0.0-rc.15" react-transition-group "2.6.1" - react-virtualized "9.21.0" - slate "0.47.8" - tinycolor2 "1.4.1" - -"@grafana/ui@^6.6.0-pre-18fda3b8b4": - version "6.6.0-pre-d9e9843a10" - resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.6.0-pre-d9e9843a10.tgz#50c53a0e1714b56a6f690cf5584b52d6a988aefd" - integrity sha512-bYu6VNagUfqxSeFXNcavCGOTJSP32S5KWUGBuGtYYtaHyNJMw2bEDzAgdpsB5yWk26jPtIA/ldbKt7itiB28YQ== - dependencies: - "@grafana/data" "6.6.0-pre-d9e9843a10" - "@grafana/slate-react" "0.22.9-grafana" - "@torkelo/react-select" "2.1.1" - "@types/react-color" "2.17.0" - "@types/slate" "0.47.1" - "@types/slate-react" "0.22.5" - bizcharts "^3.5.5" - classnames "2.2.6" - d3 "5.9.1" - immutable "3.8.2" - jquery "3.4.1" - lodash "4.17.15" - moment "2.24.0" - papaparse "4.6.3" - rc-cascader "0.17.5" - rc-drawer "3.0.2" - rc-time-picker "^3.7.2" - react "16.12.0" - react-calendar "2.18.1" - react-color "2.17.0" - react-custom-scrollbars "4.2.1" - react-dom "16.12.0" - react-highlight-words "0.11.0" - react-popper "1.3.3" - react-storybook-addon-props-combinations "1.1.0" - react-table "7.0.0-rc.15" - react-transition-group "2.6.1" - react-virtualized "9.21.0" slate "0.47.8" tinycolor2 "1.4.1" @@ -1444,52 +1322,24 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== +"@popperjs/core@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.0.tgz#0e1bdf8d021e7ea58affade33d9d607e11365915" + integrity sha512-NMrDy6EWh9TPdSRiHmHH2ye1v5U0gBD7pRYwSwJvomx7Bm4GG04vu63dYiVzebLOx2obPpJugew06xVP0Nk7hA== + +"@torkelo/react-select@3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@torkelo/react-select/-/react-select-3.0.8.tgz#04bfc877118af425f97eac2b471b66705484ee4a" + integrity sha512-becmEGnaOQpUcZS7kjQLaxjY0WKJcFFvAOTWIiU1XfwBV1sdCSgYFGWT+QhkCdRlP2Ux5U4cKhTUsWSeI9FsIA== dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" - -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== - dependencies: - "@nodelib/fs.stat" "2.0.3" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== - -"@nodelib/fs.stat@^1.1.2": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" - integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== - dependencies: - "@nodelib/fs.scandir" "2.1.3" - fastq "^1.6.0" - -"@torkelo/react-select@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@torkelo/react-select/-/react-select-2.1.1.tgz#0ca7027b4429816178df81e33ad0894699e262f1" - integrity sha512-dt+S8Myn+1Wo/UJ/kQJzDa7ztd7dpL4ueT0eMFqsGRdvMobl9xathBUZu5YMNpz7byFltrYJaPMotnPHd13rtg== - dependencies: - classnames "^2.2.5" - emotion "^9.1.2" - memoize-one "^4.0.0" + "@babel/runtime" "^7.4.4" + "@emotion/cache" "^10.0.9" + "@emotion/core" "^10.0.9" + "@emotion/css" "^10.0.9" + memoize-one "^5.0.0" prop-types "^15.6.0" - raf "^3.4.0" - react-input-autosize "^2.2.1" - react-transition-group "^2.2.1" + react-input-autosize "^2.2.2" + react-transition-group "^4.3.0" "@types/anymatch@*": version "1.3.1" @@ -1633,6 +1483,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/flatbuffers@^1.9.1": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@types/flatbuffers/-/flatbuffers-1.10.0.tgz#aa74e30ffdc86445f2f060e1808fc9d56b5603ba" + integrity sha512-7btbphLrKvo5yl/5CC2OCxUSMx1wV1wvGT1qDXkSt7yi00/YW7E8k6qzXqJHsp+WU0eoG7r6MTQQXI9lIvd0qA== + "@types/grafana@github:CorpGlory/types-grafana": version "4.6.3" resolved "https://codeload.github.com/CorpGlory/types-grafana/tar.gz/db4a6da572e42d78e27b753725034dce66dcfebb" @@ -1779,6 +1634,11 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.0.tgz#a2502fb7ce9b6626fdbfc2e2a496f472de1bdd05" integrity sha512-gDE8JJEygpay7IjA/u3JiIURvwZW08f0cZSZLAzFoX/ZmeqvS0Sqv+97aKuHpNsalAMMhwPe+iAS6fQbfmbt7A== +"@types/node@^12.0.4": + version "12.12.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.30.tgz#3501e6f09b954de9c404671cefdbcc5d9d7c45f6" + integrity sha512-sz9MF/zk6qVr3pAnM0BSQvYIBK44tS75QC5N+VbWSE4DjCV/pJ+UzCW/F+vVnl7TkOPcuwQureKNtSSwjBTaMg== + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -1815,25 +1675,44 @@ dependencies: "@types/react" "*" -"@types/react-dev-utils@^9.0.1": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@types/react-dev-utils/-/react-dev-utils-9.0.2.tgz#5432b9d741fab0371edc2371ffa3ca14756d8bf4" - integrity sha512-TmPwzYFdybS1nEoVrITlIrXiuHZAaUzez50tUr8/78QxvT573F2pYQXqH2xT61NOitgjwssbQBeIhUDC0dHtqw== - dependencies: - "@types/eslint" "*" - "@types/express" "*" - "@types/html-webpack-plugin" "*" - "@types/webpack" "*" - "@types/webpack-dev-server" "*" - -"@types/react-dom@*", "@types/react-dom@^16.9.4": - version "16.9.4" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df" - integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw== +"@types/react-dom@*": + version "16.9.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7" + integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg== dependencies: "@types/react" "*" -"@types/react-select@2.0.15": +"@types/react-dom@^16.0.11": + version "16.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.11.tgz#bd10ccb0d9260343f4b9a49d4f7a8330a5c1f081" + integrity sha512-x6zUx9/42B5Kl2Vl9HlopV8JF64wLpX3c+Pst9kc1HgzrsH+mkehe/zmHMQTplIrR48H2gpU7ZqurQolYu8XBA== + dependencies: + "@types/react" "*" + +"@types/react-select@3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-3.0.8.tgz#b824a12d438dd493c30ffff49a805f797602a837" + integrity sha512-0763TXYZc8bTiHM+DUnWoy9Rg5mk6PxYWBrEe6Fkjgc0Kv0r1RqjZk9/BrK4wdM0RNjYjixlFPnUhOJb76sMGg== + dependencies: + "@types/react" "*" + "@types/react-dom" "*" + "@types/react-transition-group" "*" + +"@types/react-table@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.2.tgz#184de5ad5a7c5aced08b49812002a4d2e8918cc0" + integrity sha512-sxvjV0JCk/ijCzENejXth99cFMnmucATaC31gz1bMk8iQwUDE2VYaw2QQTcDrzBxzastBQGdcLpcFIN61RvgIA== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@*": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.4.tgz#c7416225987ccdb719262766c1483da8f826838d" + integrity sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^2.0.15": version "2.0.15" resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.15.tgz#51d607667f59a12e980abcc5bbf9636307293e44" integrity sha512-lbtGCfZ82lKAU0KPoO6M81ZqoT3cOOLWTNkwgmPlBekNBt95ccWItAIKiGZnoO7+gzk413biIxetRSM2CoLL8w== @@ -1934,82 +1813,15 @@ "@types/react" "*" immutable "^3.8.2" -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== - "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== -"@types/tapable@*": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" - integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== - -"@types/through@*": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.29.tgz#72943aac922e179339c651fa34a4428a4d722f93" - integrity sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w== - dependencies: - "@types/node" "*" - -"@types/tmp@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd" - integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== - -"@types/uglify-js@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" - integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== - dependencies: - source-map "^0.6.1" - -"@types/webpack-dev-server@*": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.9.0.tgz#e31096477a88b0e54968cbc0d688dac9ba2c5442" - integrity sha512-4wXREDfUJmKTNcoaLLHjgsRHZhogIScXJPc5B6e5bYx16zd9H3WfM67w+mEgNaRxVCgb6YNnc8O2lX2IUn4zdQ== - dependencies: - "@types/connect-history-api-fallback" "*" - "@types/express" "*" - "@types/http-proxy-middleware" "*" - "@types/serve-static" "*" - "@types/webpack" "*" - -"@types/webpack-sources@*": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" - integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.6.1" - -"@types/webpack@*": - version "4.41.2" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.2.tgz#c6faf0111de27afdffe1158dac559e447c273516" - integrity sha512-DNMQOfEvwzWRRyp6Wy9QVCgJ3gkelZsuBE2KUD318dg95s9DKGiT5CszmmV58hq8jk89I9NClre48AEy1MWAJA== - dependencies: - "@types/anymatch" "*" - "@types/node" "*" - "@types/tapable" "*" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - source-map "^0.6.0" - -"@types/webpack@4.4.34": - version "4.4.34" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.34.tgz#e5f88b9a795da11683b4ec4a07d1c2b023b19810" - integrity sha512-GnEBgjHsfO1M7DIQ0dAupSofcmDItE3Zsu3reK8SQpl/6N0rtUQxUmQzVFAS5ou/FGjsYKjXAWfItLZ0kNFTfQ== - dependencies: - "@types/anymatch" "*" - "@types/node" "*" - "@types/tapable" "*" - "@types/uglify-js" "*" - source-map "^0.6.0" +"@types/text-encoding-utf-8@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.1.tgz#908d884af1114e5d8df47597b1e04f833383d23d" + integrity sha512-GpIEYaS+yNfYqpowLLziiY42pyaL+lThd/wMh6tTubaKuG4IRkXqqyxK7Nddn3BvpUg2+go3Gv/jbXvAFMRjiQ== "@types/yargs-parser@*": version "13.1.0" @@ -2290,11 +2102,6 @@ align-text@^0.1.1, align-text@^0.1.3: longest "^1.0.1" repeat-string "^1.5.2" -alphanum-sort@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= - alter@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/alter/-/alter-0.2.0.tgz#c7588808617572034aae62480af26b1d4d1cb3cd" @@ -2370,6 +2177,22 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +apache-arrow@0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/apache-arrow/-/apache-arrow-0.15.1.tgz#136c03e18c3fa2617b41999608e7e685b0966147" + integrity sha512-3H+sC789nWn8JDnMwfd2j19NJ4gMcdtbpp2Haa22wBoDGUbbA5FgD2OqfE9Mr4yPlJZFWVJDw7C1hgJo2UolxA== + dependencies: + "@types/flatbuffers" "^1.9.1" + "@types/node" "^12.0.4" + "@types/text-encoding-utf-8" "^1.0.1" + command-line-args "5.0.2" + command-line-usage "5.0.5" + flatbuffers "1.11.0" + json-bignum "^0.0.3" + pad-left "^2.1.0" + text-encoding-utf-8 "^1.0.2" + tslib "^1.9.3" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2395,6 +2218,14 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argv-tools@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/argv-tools/-/argv-tools-0.1.2.tgz#fc4918a70775b8cc5f8296fa0cfea137bd8a8229" + integrity sha512-wxqoymY0BEu9NblZVQiOTOAiJUjPhaa/kbNMjC2h6bnrmUSgnxKgWJo3lzXvi3bHJRwXyqK/dHzMlZVRT89Cxg== + dependencies: + array-back "^2.0.0" + find-replace "^2.0.1" + argv@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab" @@ -2415,6 +2246,13 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-back@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022" + integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw== + dependencies: + typical "^2.6.1" + array-differ@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" @@ -2702,24 +2540,6 @@ babel-plugin-emotion@^10.0.27: find-root "^1.1.0" source-map "^0.5.7" -babel-plugin-emotion@^9.2.11: - version "9.2.11" - resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" - integrity sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@emotion/babel-utils" "^0.6.4" - "@emotion/hash" "^0.6.2" - "@emotion/memoize" "^0.6.1" - "@emotion/stylis" "^0.7.0" - babel-plugin-macros "^2.0.0" - babel-plugin-syntax-jsx "^6.18.0" - convert-source-map "^1.5.0" - find-root "^1.1.0" - mkdirp "^0.5.1" - source-map "^0.5.7" - touch "^2.0.1" - babel-plugin-istanbul@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" @@ -2907,11 +2727,12 @@ bindings@^1.5.0: file-uri-to-path "1.0.0" bizcharts@^3.5.5: - version "3.5.6" - resolved "https://registry.yarnpkg.com/bizcharts/-/bizcharts-3.5.6.tgz#721aec217a7e9fda4abd186e7be81161e8bb97df" - integrity sha512-N9MVybqfqAEPmZwITyWu/bTZldK4sJHqg9nmTZ6bh6c+YUNs7OAGcjIxyrNfh4HOlxl6+8Jy5Vi55zCeOHxa9w== + version "3.5.9" + resolved "https://registry.yarnpkg.com/bizcharts/-/bizcharts-3.5.9.tgz#b4c56c8bc5e8567f65748aeb3916902c4e9c98c0" + integrity sha512-1GI1SWNHfU3xRYGh4b4Dn6gfHMaOZnl0EXewZGEL5V5/m97k2kBonedA0LvtdrOQZRAAM+sP1uwny/ttkNsnEQ== dependencies: - "@antv/g2" "3.5.9" + "@antv/g2" "3.5.15" + "@babel/runtime" "^7.7.6" invariant "^2.2.2" lodash.debounce "^4.0.8" prop-types "^15.6.0" @@ -3283,15 +3104,6 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -3371,7 +3183,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.2.6, classnames@2.x, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6: +classnames@2.2.6, classnames@2.x, classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -3601,10 +3413,26 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -command-exists@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.8.tgz#715acefdd1223b9c9b37110a149c6392c2852291" - integrity sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw== +command-line-args@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.0.2.tgz#c4e56b016636af1323cf485aa25c3cb203dfbbe4" + integrity sha512-/qPcbL8zpqg53x4rAaqMFlRV4opN3pbla7I7k9x8kyOBMQoGT6WltjN6sXZuxOXw6DgdK7Ad+ijYS5gjcr7vlA== + dependencies: + argv-tools "^0.1.1" + array-back "^2.0.0" + find-replace "^2.0.1" + lodash.camelcase "^4.3.0" + typical "^2.6.1" + +command-line-usage@5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-5.0.5.tgz#5f25933ffe6dedd983c635d38a21d7e623fda357" + integrity sha512-d8NrGylA5oCXSbGoKz05FkehDAzSmIm4K03S5VDh4d5lZAtTWfc3D1RuETtuQCn8129nYfJfDdF7P/lwcz1BlA== + dependencies: + array-back "^2.0.0" + chalk "^2.4.1" + table-layout "^0.4.3" + typical "^2.6.1" commander@2: version "2.20.1" @@ -3719,11 +3547,28 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= +content-type-parser@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" + integrity sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ== + contour_plot@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/contour_plot/-/contour_plot-0.0.1.tgz#475870f032b8e338412aa5fc507880f0bf495c77" integrity sha1-R1hw8DK44zhBKqX8UHiA8L9JXHc= +convert-source-map@^1.4.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + integrity sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU= + +convert-source-map@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -3830,18 +3675,15 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-emotion@^9.2.12: - version "9.2.12" - resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" - integrity sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA== +create-emotion@^10.0.27: + version "10.0.27" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503" + integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg== dependencies: - "@emotion/hash" "^0.6.2" - "@emotion/memoize" "^0.6.1" - "@emotion/stylis" "^0.7.0" - "@emotion/unitless" "^0.6.2" - csstype "^2.5.2" - stylis "^3.5.0" - stylis-rule-sheet "^0.0.10" + "@emotion/cache" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" create-hash@^1.1.0, create-hash@^1.1.2: version "1.2.0" @@ -4155,10 +3997,15 @@ csstype@^2.2.0, csstype@^2.5.7: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== -csstype@^2.5.2: - version "2.6.7" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" - integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== +csstype@^2.5.7: + version "2.6.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" + integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== + +csstype@^2.6.7: + version "2.6.9" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" + integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== cst@^0.4.3: version "0.4.10" @@ -4259,6 +4106,11 @@ d3-ease@1, d3-ease@~1.0.3: resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.6.tgz#ebdb6da22dfac0a22222f2d4da06f66c416a0ec0" integrity sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ== +d3-ease@~1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.6.tgz#ebdb6da22dfac0a22222f2d4da06f66c416a0ec0" + integrity sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ== + d3-fetch@1: version "1.1.2" resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.1.2.tgz#957c8fbc6d4480599ba191b1b2518bf86b3e1be2" @@ -4308,9 +4160,9 @@ d3-interpolate@~1.1.5: d3-color "1" d3-path@1: - version "1.0.9" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" - integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + version "1.0.8" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.8.tgz#4a0606a794d104513ec4a8af43525f374b278719" + integrity sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg== d3-polygon@1: version "1.0.5" @@ -4352,6 +4204,11 @@ d3-selection@1, d3-selection@^1.0.2, d3-selection@^1.1.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.1.tgz#98eedbbe085fbda5bafa2f9e3f3a2f4d7d622a98" integrity sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA== +d3-selection@^1.0.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.1.tgz#98eedbbe085fbda5bafa2f9e3f3a2f4d7d622a98" + integrity sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA== + d3-shape@1: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" @@ -4376,7 +4233,24 @@ d3-timer@1, d3-timer@~1.0.6: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== -d3-transition@1, d3-transition@^1.0.1: +d3-timer@~1.0.6: + version "1.0.10" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" + integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== + +d3-transition@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.2.0.tgz#f538c0e21b2aa1f05f3e965f8567e81284b3b2b8" + integrity sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-transition@^1.0.1: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398" integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA== @@ -4441,43 +4315,6 @@ d3@5.15.0: d3-voronoi "1" d3-zoom "1" -d3@5.9.1: - version "5.9.1" - resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.1.tgz#fde73fa9af7281d2ff0d2a32aa8f306e93a6d1cd" - integrity sha512-JceuBn5VVWySPQc9EA0gfq0xQVgEQXGokHhe+359bmgGeUITLK2r2b9idMzquQne9DKxb7JDCE1gDRXe9OIF2Q== - dependencies: - d3-array "1" - d3-axis "1" - d3-brush "1" - d3-chord "1" - d3-collection "1" - d3-color "1" - d3-contour "1" - d3-dispatch "1" - d3-drag "1" - d3-dsv "1" - d3-ease "1" - d3-fetch "1" - d3-force "1" - d3-format "1" - d3-geo "1" - d3-hierarchy "1" - d3-interpolate "1" - d3-path "1" - d3-polygon "1" - d3-quadtree "1" - d3-random "1" - d3-scale "2" - d3-scale-chromatic "1" - d3-selection "1" - d3-shape "1" - d3-time "1" - d3-time-format "2" - d3-timer "1" - d3-transition "1" - d3-voronoi "1" - d3-zoom "1" - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -4601,18 +4438,28 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deep-equal@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -4760,13 +4607,21 @@ dom-css@^2.0.0: prefix-style "2.0.1" to-camel-case "1.0.0" -"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.3.1, dom-helpers@^3.4.0: +dom-helpers@^3.3.1: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== dependencies: "@babel/runtime" "^7.1.2" +dom-helpers@^5.0.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" + integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== + dependencies: + "@babel/runtime" "^7.6.3" + csstype "^2.6.7" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -4820,21 +4675,6 @@ domutils@1.5, domutils@1.5.1: dom-serializer "0" domelementtype "1" -domutils@^1.5.1, domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dot-prop@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" - integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== - dependencies: - is-obj "^1.0.0" - dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -4842,11 +4682,6 @@ dotignore@~0.1.2: dependencies: minimatch "^3.0.4" -duplexer@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= - duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -4908,13 +4743,13 @@ emojis-list@^2.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= -emotion@^9.1.2: - version "9.2.12" - resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" - integrity sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ== +emotion@10.0.27: + version "10.0.27" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e" + integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g== dependencies: - babel-plugin-emotion "^9.2.11" - create-emotion "^9.2.12" + babel-plugin-emotion "^10.0.27" + create-emotion "^10.0.27" empower-core@^1.2.0: version "1.2.0" @@ -4984,10 +4819,27 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.16.3, es-abstract@^1.17.0-next.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.0.tgz#f42a517d0036a5591dbb2c463591dc8bb50309b1" - integrity sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug== +es-abstract@^1.17.0-next.1: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + +es-abstract@^1.5.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" @@ -5023,6 +4875,15 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -5420,6 +5281,14 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-replace@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-2.0.1.tgz#6d9683a7ca20f8f9aabeabad07e4e2580f528550" + integrity sha512-LzDo3Fpa30FLIBsh6DCDnMN1KW2g4QKkqKmejlImgWY67dDFPX/x9Kh/op/GK522DchQXEvDi/wD48HKW49XOQ== + dependencies: + array-back "^2.0.0" + test-value "^3.0.0" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -5495,7 +5364,12 @@ flagged-respawn@^1.0.0: resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== -flatten@^1.0.2: +flatbuffers@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-1.11.0.tgz#90a47e584dd7851ad7a913f5a0ee99c1d76ce59f" + integrity sha512-0PqFKtXI4MjxomI7jO4g5XfLPm/15g2R+5WGCHBGYGh0ihQiypnHlJ6bMmkkrAe0GzZ4d7PDAfCONKIPUxNF+A== + +flush-write-stream@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== @@ -5533,6 +5407,24 @@ for-each@~0.3.3: dependencies: is-callable "^1.1.3" +fmin@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/fmin/-/fmin-0.0.2.tgz#59bbb40d43ffdc1c94cd00a568c41f95f1973017" + integrity sha1-Wbu0DUP/3ByUzQClaMQflfGXMBc= + dependencies: + contour_plot "^0.0.1" + json2module "^0.0.3" + rollup "^0.25.8" + tape "^4.5.1" + uglify-js "^2.6.2" + +for-each@~0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -5691,7 +5583,7 @@ get-stream@^4.0.0: dependencies: pump "^3.0.0" -get-user-locale@^1.1.1: +get-user-locale@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-1.3.0.tgz#21ea740e413541281ae7b2b42e70ee523b7725b2" integrity sha512-c5N8P0upjxCF9unIC2vTA+B+8nN7kU/D/TeItMAVYhWIIksyoULM1aflKflXM3w+Ij6vF/JZys+QcwIoDuy3Ag== @@ -5765,7 +5657,7 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, gl once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.4, glob@^7.1.6, glob@~7.1.6: +glob@^7.1.4, glob@~7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -6090,7 +5982,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.0, has@^1.0.3, has@~1.0.3: +has@^1.0.1, has@^1.0.3, has@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -6314,21 +6206,6 @@ ignore@^3.3.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== -ignore@^5.1.1: - version "5.1.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== - -image-size@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= - -immer@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" - integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== - immutable@3.8.2, immutable@^3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" @@ -6414,7 +6291,17 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -6563,7 +6450,7 @@ is-buffer@^2.0.2: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5: +is-callable@^1.1.1, is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== @@ -6750,6 +6637,13 @@ is-regex@^1.0.4, is-regex@^1.0.5, is-regex@~1.0.5: dependencies: has "^1.0.3" +is-regex@^1.0.5, is-regex@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== + dependencies: + has "^1.0.3" + is-relative@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" @@ -6777,19 +6671,7 @@ is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-string@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-svg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" - integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== - dependencies: - html-comment-regex "^1.1.0" - -is-symbol@^1.0.2: +is-symbol@^1.0.1, is-symbol@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== @@ -7514,6 +7396,11 @@ jshint@^2.9.6: shelljs "0.3.x" strip-json-comments "1.0.x" +json-bignum@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/json-bignum/-/json-bignum-0.0.3.tgz#41163b50436c773d82424dbc20ed70db7604b8d7" + integrity sha1-QRY7UENsdz2CQk28IO1w23YEuNc= + json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -7541,15 +7428,10 @@ json2module@^0.0.3: dependencies: rw "^1.3.2" -json3@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@2.x, json5@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" - integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== +json5@2.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== dependencies: minimist "^1.2.0" @@ -7612,14 +7494,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -last-call-webpack-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" - integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== - dependencies: - lodash "^4.17.5" - webpack-sources "^1.1.0" - lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" @@ -7784,10 +7658,10 @@ lodash._getnative@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= lodash.debounce@^4.0.8: version "4.0.8" @@ -7823,6 +7697,11 @@ lodash.once@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.padend@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -7865,19 +7744,12 @@ log-symbols@^1.0.0: dependencies: chalk "^1.0.0" -log-symbols@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -8006,15 +7878,12 @@ memoize-one@^4.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" +memoize-one@^5.0.0, memoize-one@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== -memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -8172,20 +8041,10 @@ minimist@~0.0.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== - dependencies: - minipass "^2.9.0" +minimist@~1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== mississippi@^3.0.0: version "3.0.0" @@ -8509,7 +8368,7 @@ nomnom@^1.5.x: dependencies: abbrev "1" -nopt@^4.0.1, nopt@~4.0.1: +nopt@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= @@ -8659,7 +8518,7 @@ object-is@^1.0.1: resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== -object-keys@^1.0.0, object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.0.0, object-keys@^1.0.11, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -8933,6 +8792,13 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pad-left@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994" + integrity sha1-FuajstRKjhOMsIOMx8tAOk/J6ZQ= + dependencies: + repeat-string "^1.5.4" + pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" @@ -10058,7 +9924,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -10254,6 +10120,20 @@ rc-drawer@3.0.2: rc-util "^4.11.2" react-lifecycles-compat "^3.0.4" +rc-slider@8.7.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.7.1.tgz#9ed07362dc93489a38e654b21b8122ad70fd3c42" + integrity sha512-WMT5mRFUEcrLWwTxsyS8jYmlaMsTVCZIGENLikHsNv+tE8ThU2lCoPfi/xFNUfJFNFSBFP3MwPez9ZsJmNp13g== + dependencies: + babel-runtime "6.x" + classnames "^2.2.5" + prop-types "^15.5.4" + rc-tooltip "^3.7.0" + rc-util "^4.0.4" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" + warning "^4.0.3" + rc-time-picker@^3.7.2: version "3.7.3" resolved "https://registry.yarnpkg.com/rc-time-picker/-/rc-time-picker-3.7.3.tgz#65a8de904093250ae9c82b02a4905e0f995e23e2" @@ -10266,7 +10146,16 @@ rc-time-picker@^3.7.2: rc-trigger "^2.2.0" react-lifecycles-compat "^3.0.4" -rc-trigger@^2.2.0: +rc-tooltip@^3.7.0: + version "3.7.3" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.3.tgz#280aec6afcaa44e8dff0480fbaff9e87fc00aecc" + integrity sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww== + dependencies: + babel-runtime "6.x" + prop-types "^15.5.8" + rc-trigger "^2.2.2" + +rc-trigger@^2.2.0, rc-trigger@^2.2.2: version "2.6.5" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885" integrity sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw== @@ -10301,22 +10190,23 @@ rc-util@^4.8.0: react-lifecycles-compat "^3.0.4" shallowequal "^0.2.2" -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== +rc-util@^4.11.2: + version "4.20.5" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.20.5.tgz#f7c77569e971ae6a8ad56f899cadd22275398325" + integrity sha512-f67s4Dt1quBYhrVPq5QMKmK3eS2hN1NNIAyhaiG0HmvqiGYAXMQ7SP2AlGqv750vnzhJs38JklbkWT1/wjhFPg== dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" + add-dom-event-listener "^1.1.0" + prop-types "^15.5.10" + react-is "^16.12.0" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" -react-calendar@2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-2.18.1.tgz#f8ef9468d8566aa0d47d9d70c88917bb2030bcb9" - integrity sha512-J3tVim1gLpnsCOaeez+z4QJB5oK6UYLJj5TSMOStSJBvkWMEcTzj7bq7yCJJCNLUg2Vd3i11gJXish0LUFhXaw== +react-calendar@2.19.2: + version "2.19.2" + resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-2.19.2.tgz#496e78eb11a00aee1ae6b5d02d221ed1ca2db952" + integrity sha512-zKbWxwmYEg84grFsCJz9EYpnGqsZy0iV67dHzkVE0EhBdXMg2eISBQYvw4+t8zdy5ySxQkDhUW8X8ERbSyZPVw== dependencies: - get-user-locale "^1.1.1" + get-user-locale "^1.2.0" merge-class-names "^1.1.1" prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" @@ -10342,38 +10232,7 @@ react-custom-scrollbars@4.2.1: prop-types "^15.5.10" raf "^3.1.0" -react-dev-utils@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.1.0.tgz#3ad2bb8848a32319d760d0a84c56c14bdaae5e81" - integrity sha512-X2KYF/lIGyGwP/F/oXgGDF24nxDA2KC4b7AFto+eqzc/t838gpSGiaU8trTqHXOohuLxxc5qi1eDzsl9ucPDpg== - dependencies: - "@babel/code-frame" "7.5.5" - address "1.1.2" - browserslist "4.7.0" - chalk "2.4.2" - cross-spawn "6.0.5" - detect-port-alt "1.1.6" - escape-string-regexp "1.0.5" - filesize "3.6.1" - find-up "3.0.0" - fork-ts-checker-webpack-plugin "1.5.0" - global-modules "2.0.0" - globby "8.0.2" - gzip-size "5.1.1" - immer "1.10.0" - inquirer "6.5.0" - is-root "2.1.0" - loader-utils "1.2.3" - open "^6.3.0" - pkg-up "2.0.0" - react-error-overlay "^6.0.3" - recursive-readdir "2.2.2" - shell-quote "1.7.2" - sockjs-client "1.4.0" - strip-ansi "5.2.0" - text-table "0.2.0" - -react-dom@16.12.0, react-dom@^16.7.0: +react-dom@16.12.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== @@ -10383,10 +10242,10 @@ react-dom@16.12.0, react-dom@^16.7.0: prop-types "^15.6.2" scheduler "^0.18.0" -react-error-overlay@^6.0.3: - version "6.0.4" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.4.tgz#0d165d6d27488e660bc08e57bdabaad741366f7a" - integrity sha512-ueZzLmHltszTshDMwyfELDq8zOA803wQ1ZuzCccXa1m57k1PxSHfflPD5W9YIiTXLs0JTLzoj6o1LuM5N6zzNA== +react-fast-compare@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.0.1.tgz#884d339ce1341aad22392e7a88664c71da48600e" + integrity sha512-C5vP0J644ofZGd54P8++O7AvrqMEbrGf8Ue0eAUJLJyw168dAX2aiYyX/zcY/eSNwO0IDjsKUaLE6n83D+TnEg== react-highlight-words@0.11.0: version "0.11.0" @@ -10396,18 +10255,33 @@ react-highlight-words@0.11.0: highlight-words-core "^1.2.0" prop-types "^15.5.8" +react-hook-form@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-4.5.3.tgz#3f9abac7bd78eedf0624d02aa9e1f8487d729e18" + integrity sha512-oQB6s3zzXbFwM8xaWEkZJZR+5KD2LwUUYTexQbpdUuFzrfs41Qg0UE3kzfzxG8shvVlzADdkYKLMXqOLWQSS/Q== + react-immutable-proptypes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" integrity sha1-Aj1vObsVyXwHHp5g0A0TbqxfoLQ= -react-input-autosize@^2.2.1: +react-input-autosize@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" integrity sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw== dependencies: prop-types "^15.5.8" +react-is@^16.12.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^16.7.0, react-is@^16.8.4: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + react-is@^16.8.1: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" @@ -10435,16 +10309,12 @@ react-popper@1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -react-popper@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.2.tgz#e723a0a7fe1c42099a13e5d494e9d7d74b352af4" - integrity sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw== +react-popper@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.3.tgz#33d425fa6975d4bd54d9acd64897a89d904b9d97" + integrity sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw== dependencies: - "@babel/runtime" "^7.1.2" - create-react-context "<=0.2.2" - popper.js "^1.14.4" - prop-types "^15.6.1" - typed-styles "^0.0.7" + react-fast-compare "^3.0.1" warning "^4.0.2" react-storybook-addon-props-combinations@1.1.0: @@ -10455,20 +10325,20 @@ react-storybook-addon-props-combinations@1.1.0: object-hash "^1.1.8" pretty-format "^21.2.1" -react-table@7.0.0-rc.15: - version "7.0.0-rc.15" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a" - integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw== - -react-table@^6.8.6: - version "6.11.5" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.11.5.tgz#84e52885db426a07a6c4ce2c7e942f2cd4e2aa58" - integrity sha512-LM+AS9v//7Y7lAlgTWW/cW6Sn5VOb3EsSkKQfQTzOW8FngB1FUskLLNEVkAYsTX9LjOWR3QlGjykJqCE6eXT/g== +react-table-6@^6.8.6: + version "6.11.0" + resolved "https://registry.yarnpkg.com/react-table-6/-/react-table-6-6.11.0.tgz#727de5d9f0357a35816a1bbc2e9c9868f8c5b2c4" + integrity sha512-zO24J+1Qg2AHxtSNMfHeGW1dxFcmLJQrAeLJyCAENdNdwJt+YolDDtJEBdZlukon7rZeAdB3d5gUH6eb9Dn5Ug== dependencies: "@types/react-table" "^6.8.5" classnames "^2.2.5" react-is "^16.8.1" +react-table@7.0.0-rc.15: + version "7.0.0-rc.15" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a" + integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw== + react-test-renderer@^16.7.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f" @@ -10489,16 +10359,6 @@ react-transition-group@2.6.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-transition-group@^2.2.1: - version "2.9.0" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" - integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== - dependencies: - dom-helpers "^3.4.0" - loose-envify "^1.4.0" - prop-types "^15.6.2" - react-lifecycles-compat "^3.0.4" - react-transition-group@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.2.tgz#9457166a9ba6ce697a3e1b076b3c049b9fb2c408" @@ -10509,17 +10369,15 @@ react-transition-group@^2.5.2: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-virtualized@9.21.0: - version "9.21.0" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.0.tgz#8267c40ffb48db35b242a36dea85edcf280a6506" - integrity sha512-duKD2HvO33mqld4EtQKm9H9H0p+xce1c++2D5xn59Ma7P8VT7CprfAe5hwjd1OGkyhqzOZiTMlTal7LxjH5yBQ== +react-transition-group@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683" + integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw== dependencies: - babel-runtime "^6.26.0" - classnames "^2.2.3" - dom-helpers "^2.4.0 || ^3.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" react@16.12.0: version "16.12.0" @@ -10530,16 +10388,6 @@ react@16.12.0: object-assign "^4.1.1" prop-types "^15.6.2" -react@^16.7.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" - integrity sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.12.0" - reactcss@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" @@ -10677,6 +10525,11 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +reduce-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" + integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc= + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -10699,6 +10552,11 @@ regenerator-runtime@^0.13.2: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -10772,7 +10630,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.5.2, repeat-string@^1.6.1: +repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -10921,13 +10779,12 @@ resolve@^1.10.0: dependencies: path-parse "^1.0.6" -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= +resolve@~1.15.1: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" + path-parse "^1.0.6" restructured@0.0.11: version "0.0.11" @@ -10961,16 +10818,6 @@ revalidator@0.1.x: resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs= -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -11051,14 +10898,14 @@ rw@1, rw@^1.3.2: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= -rxjs@^6.3.3, rxjs@^6.4.0: +rxjs@6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -11219,6 +11066,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -11271,13 +11123,6 @@ shallow-clone@^1.0.0: kind-of "^5.0.0" mixin-object "^2.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shallow-equal@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" @@ -11573,16 +11418,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= - spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -11761,15 +11596,6 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - string.prototype.trim@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" @@ -11795,7 +11621,7 @@ string.prototype.trimright@^2.1.1: define-properties "^1.1.3" function-bind "^1.1.1" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -11908,25 +11734,6 @@ style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" -stylehacks@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" - integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -stylis-rule-sheet@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" - integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== - -stylis@^3.5.0: - version "3.5.4" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" - integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -11987,15 +11794,36 @@ systemjs@0.20.19: resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-0.20.19.tgz#c2b9e79c19f4bea53a19b1ed3f974ffb463be949" integrity sha512-H/rKwNEEyej/+IhkmFNmKFyJul8tbH/muiPq5TyNoVTwsGhUjRsN3NlFnFQUvFXA3+GQmsXkCNXU6QKPl779aw== +systemjs-plugin-css@0.1.37: + version "0.1.37" + resolved "https://registry.yarnpkg.com/systemjs-plugin-css/-/systemjs-plugin-css-0.1.37.tgz#684847252ca69b7da24a1201094c86274324e82f" + integrity sha512-wCGG62zYXuOlNji5FlBjeMFAnLeAO/HQmFg+8UBX/mlHoAKLHlGFYRstlhGKibRU2oxk/BH9DaihOuhhNLi7Kg== + +systemjs@0.20.19: + version "0.20.19" + resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-0.20.19.tgz#c2b9e79c19f4bea53a19b1ed3f974ffb463be949" + integrity sha512-H/rKwNEEyej/+IhkmFNmKFyJul8tbH/muiPq5TyNoVTwsGhUjRsN3NlFnFQUvFXA3+GQmsXkCNXU6QKPl779aw== + +table-layout@^0.4.3: + version "0.4.5" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378" + integrity sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw== + dependencies: + array-back "^2.0.0" + deep-extend "~0.6.0" + lodash.padend "^4.6.1" + typical "^2.6.1" + wordwrapjs "^3.0.0" + tapable@^1.0.0, tapable@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tape@^4.5.1: - version "4.13.0" - resolved "https://registry.yarnpkg.com/tape/-/tape-4.13.0.tgz#e2f581ff5f12a7cbd787e9f83c76c2851782fce2" - integrity sha512-J/hvA+GJnuWJ0Sj8Z0dmu3JgMNU+MmusvkCT7+SN4/2TklW18FNCp/UuHIEhPZwHfy4sXfKYgC7kypKg4umbOw== + version "4.13.2" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.13.2.tgz#eb419b9d9bc004025b1a81a5b63093e07f425629" + integrity sha512-waWwC/OqYVE9TS6r1IynlP2sEdk4Lfo6jazlgkuNkPTHIbuG2BTABIaKdlQWwPeB6Oo4ksZ1j33Yt0NTOAlYMQ== dependencies: deep-equal "~1.1.1" defined "~1.0.0" @@ -12008,7 +11836,7 @@ tape@^4.5.1: is-regex "~1.0.5" minimist "~1.2.0" object-inspect "~1.7.0" - resolve "~1.14.2" + resolve "~1.15.1" resumer "~0.0.0" string.prototype.trim "~1.2.1" through "~2.3.8" @@ -12078,6 +11906,14 @@ test-exclude@^5.2.3: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" +test-value@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/test-value/-/test-value-3.0.0.tgz#9168c062fab11a86b8d444dd968bb4b73851ce92" + integrity sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ== + dependencies: + array-back "^2.0.0" + typical "^2.6.1" + tether-drop@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/tether-drop/-/tether-drop-1.4.2.tgz#28e240cce077f4ae1d8e5990a03b71e0cd6fbfec" @@ -12090,7 +11926,12 @@ tether@^1.1.0: resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.7.tgz#d56a818590d8fe72e387f77a67f93ab96d8e1fb2" integrity sha512-Z0J1aExjoFU8pybVkQAo/vD2wfSO63r+XOPfWQMC5qtf1bI7IWqNk4MiyBcgvvnY8kqnY06dVdvwTK2S3PU/Fw== -text-table@0.2.0, text-table@^0.2.0: +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + +text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= @@ -12108,7 +11949,7 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@^2.3.6, through@~2.3.4, through@~2.3.6, through@~2.3.8: +through@2, through@~2.3.4, through@~2.3.6, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -12228,17 +12069,14 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" -toposort@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" - integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= - -touch@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" - integrity sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A== +tough-cookie@>=2.3.3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" + integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== dependencies: - nopt "~1.0.10" + ip-regex "^2.1.0" + psl "^1.1.28" + punycode "^2.1.1" tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" @@ -12363,6 +12201,11 @@ tslint-config-prettier@^1.18.0: resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== +tslib@^1.9.3: + version "1.11.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" + integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + tslint@5.20.1: version "5.20.1" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" @@ -12433,10 +12276,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" - integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== +typescript@^3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" + integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== + +typical@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" + integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= ua-parser-js@^0.7.18: version "0.7.20" @@ -12461,6 +12309,21 @@ uglify-js@^2.6.2: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.1.4: + version "3.6.2" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.2.tgz#fd8048c86d990ddd29fe99d3300e0cb329103f4d" + integrity sha512-+gh/xFte41GPrgSMJ/oJVq15zYmqr74pY9VoM69UzMzq9NFk4YDylclb1/bhEzZSaUQjbW5RvniHeq1cdtRYjw== + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -12701,11 +12564,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -vendors@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.3.tgz#a6467781abd366217c050f8202e7e50cc9eef8c0" - integrity sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw== - venn.js@~0.2.20: version "0.2.20" resolved "https://registry.yarnpkg.com/venn.js/-/venn.js-0.2.20.tgz#3f0e50cc75cba1f58692a8a32f67bd7aaf1aa6fa" @@ -12781,13 +12639,20 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" -warning@^4.0.1, warning@^4.0.2: +warning@^4.0.1, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== dependencies: loose-envify "^1.0.0" +warning@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" + integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + dependencies: + loose-envify "^1.0.0" + watchpack@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" @@ -13037,11 +12902,34 @@ wordwrap@0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= +wolfy87-eventemitter@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wolfy87-eventemitter/-/wolfy87-eventemitter-5.1.0.tgz#35c1ac0dd1ac0c15e35d981508fc22084a13a011" + integrity sha1-NcGsDdGsDBXjXZgVCPwiCEoToBE= + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= + wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wordwrapjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e" + integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw== + dependencies: + reduce-flatten "^1.0.1" + typical "^2.6.1" + worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"