Migrate query editor to react (#1520)

* Initial react query editor

* CI: run checks on all branches

* Update react packages

* Initial metric picker

* Load metrics

* Tweak styles

* Add variables to metric options

* Tweak styles

* Filtering and keyboard navigation

* Open menu with keyboard

* Update function editor

* Move functions in editor

* Add function component

* Edit func params

* Push alias functions to the end

* Tweak labels size

* Fix menu position

* Metric options editor

* Fix css styles building

* More work on query options

* Fix tests

* Refactor: extract metrics query editor and functions editor

* Refactor: move things around

* Text metrics editor

* Problems query editor

* Problems mode options

* Item id query editor

* IT services query editor

* Triggers query editor

* Refactor: remove unused

* remove derprecated theme usage

* Load proxy options

* Fetch metric options on variable change

* Remove angular query editor

* Migrate annotations editor to react

* Fix tests
This commit is contained in:
Alexander Zobnin
2022-11-09 17:50:13 +03:00
committed by GitHub
parent f765d47fed
commit 504c9af226
44 changed files with 7822 additions and 5868 deletions

View File

@@ -2,8 +2,6 @@ name: CI
on: on:
push: push:
branches:
- master
pull_request: pull_request:
branches: branches:
- master - master
@@ -16,12 +14,12 @@ jobs:
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.1.2 uses: actions/setup-node@v2.1.2
with: with:
node-version: "16.x" node-version: '16.x'
- name: Setup Go environment - name: Setup Go environment
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: "1.17" go-version: '1.17'
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
@@ -61,12 +59,12 @@ jobs:
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.1.2 uses: actions/setup-node@v2.1.2
with: with:
node-version: "16.x" node-version: '16.x'
- name: Setup Go environment - name: Setup Go environment
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: "1.17" go-version: '1.17'
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
@@ -105,12 +103,12 @@ jobs:
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.1.2 uses: actions/setup-node@v2.1.2
with: with:
node-version: "16.x" node-version: '16.x'
- name: Setup Go environment - name: Setup Go environment
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: "1.17" go-version: '1.17'
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path

View File

@@ -30,18 +30,18 @@
"@babel/preset-react": "7.6.3", "@babel/preset-react": "7.6.3",
"@emotion/css": "11.1.3", "@emotion/css": "11.1.3",
"@emotion/react": "11.1.5", "@emotion/react": "11.1.5",
"@grafana/data": "^8.3.6", "@grafana/data": "9.1.2",
"@grafana/runtime": "^8.3.6", "@grafana/runtime": "9.1.2",
"@grafana/toolkit": "^8.3.6", "@grafana/toolkit": "9.1.2",
"@grafana/ui": "^8.3.6", "@grafana/ui": "9.1.2",
"@popperjs/core": "2.4.0", "@popperjs/core": "2.4.0",
"@types/classnames": "2.2.9", "@types/classnames": "2.2.9",
"@types/grafana": "github:CorpGlory/types-grafana", "@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "24.0.13", "@types/jest": "24.0.13",
"@types/jquery": "3.3.32", "@types/jquery": "3.3.32",
"@types/lodash": "4.14.161", "@types/lodash": "4.14.161",
"@types/react": "16.8.16", "@types/react": "17.0.42",
"@types/react-dom": "16.8.4", "@types/react-dom": "17.0.14",
"@types/react-transition-group": "4.2.4", "@types/react-transition-group": "4.2.4",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-jest": "24.8.0", "babel-jest": "24.8.0",
@@ -69,12 +69,13 @@
"ng-annotate-webpack-plugin": "0.3.0", "ng-annotate-webpack-plugin": "0.3.0",
"node-sass": "^7.0.1", "node-sass": "^7.0.1",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react": "16.12.0", "react": "17.0.2",
"react-dom": "16.12.0", "react-dom": "17.0.2",
"react-popper": "^2.2.3", "react-popper": "^2.2.3",
"react-table-6": "^6.8.6", "react-table-6": "^6.8.6",
"react-test-renderer": "^16.7.0", "react-test-renderer": "17.0.2",
"react-transition-group": "4.3.0", "react-transition-group": "4.3.0",
"react-use": "17.4.0",
"rst2html": "github:thoward/rst2html#990cb89", "rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"sass-loader": "10.2.1", "sass-loader": "10.2.1",

View File

@@ -0,0 +1,141 @@
import { css, cx } from '@emotion/css';
import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react';
import { ClickOutsideWrapper, Icon, Input, Spinner, useStyles2 } from '@grafana/ui';
import { MetricPickerMenu } from './MetricPickerMenu';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { isRegex } from '../../datasource-zabbix/utils';
export interface Props {
value: string;
isLoading?: boolean;
options: SelectableValue<string>[];
width?: number;
onChange: (value: string) => void;
}
export const MetricPicker = ({ value, options, isLoading, width, onChange }: Props): JSX.Element => {
const [isOpen, setOpen] = useState(false);
const [query, setQuery] = useState(value);
const [filteredOptions, setFilteredOptions] = useState(options);
const [selectedOptionIdx, setSelectedOptionIdx] = useState(-1);
const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 });
const ref = useRef<HTMLDivElement>(null);
const customStyles = useStyles2(getStyles);
const inputClass = cx({
[customStyles.inputRegexp]: isRegex(query),
[customStyles.inputVariable]: query.startsWith('$'),
});
useEffect(() => {
setFilteredOptions(options);
}, [options]);
const onOpen = () => {
setOpen(true);
setFilteredOptions(options);
};
const onClose = useCallback(() => {
setOpen(false);
}, []);
// Only call onClose if menu is open. Prevent unnecessary calls for multiple pickers on the page.
const onClickOutside = () => isOpen && onClose();
const onInputChange = (v: FormEvent<HTMLInputElement>) => {
if (!isOpen) {
setOpen(true);
}
const newQuery = v?.currentTarget?.value;
if (newQuery) {
setQuery(newQuery);
if (value != newQuery) {
const filtered = options.filter(
(option) =>
option.value?.toLowerCase().includes(newQuery.toLowerCase()) ||
option.label?.toLowerCase().includes(newQuery.toLowerCase())
);
setFilteredOptions(filtered);
} else {
setFilteredOptions(options);
}
} else {
setQuery('');
setFilteredOptions(options);
}
};
const onMenuOptionSelect = (option: SelectableValue<string>) => {
const newValue = option?.value || '';
setQuery(newValue);
onChange(newValue);
onClose();
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
if (!isOpen) {
setOpen(true);
}
e.preventDefault();
e.stopPropagation();
const selected = selectedOptionIdx < filteredOptions.length - 1 ? selectedOptionIdx + 1 : 0;
setSelectedOptionIdx(selected);
} else if (e.key === 'ArrowUp') {
if (!isOpen) {
setOpen(true);
}
e.preventDefault();
e.stopPropagation();
const selected = selectedOptionIdx > 0 ? selectedOptionIdx - 1 : filteredOptions.length - 1;
setSelectedOptionIdx(selected);
} else if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
onMenuOptionSelect(filteredOptions[selectedOptionIdx]);
}
};
return (
<div data-testid="role-picker" style={{ position: 'relative' }} ref={ref}>
<ClickOutsideWrapper onClick={onClickOutside}>
<Input
className={inputClass}
value={query}
type="text"
onChange={onInputChange}
onBlur={() => onChange(query)}
onMouseDown={onOpen}
suffix={isLoading && <Spinner />}
width={width}
onKeyDown={onKeyDown}
/>
{isOpen && (
<MetricPickerMenu
options={filteredOptions}
onSelect={onMenuOptionSelect}
offset={offset}
minWidth={width}
selected={selectedOptionIdx}
/>
)}
</ClickOutsideWrapper>
</div>
);
};
export const getStyles = (theme: GrafanaTheme2) => {
return {
inputRegexp: css`
input {
color: ${theme.colors.warning.main};
}
`,
inputVariable: css`
input {
color: ${theme.colors.primary.text};
}
`,
};
};

View File

@@ -0,0 +1,138 @@
import { css, cx } from '@emotion/css';
import React, { FormEvent } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { CustomScrollbar, getSelectStyles, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
import { MENU_MAX_HEIGHT } from './constants';
interface Props {
options: SelectableValue<string>[];
onSelect: (option: SelectableValue<string>) => void;
offset: { vertical: number; horizontal: number };
minWidth?: number;
selected?: number;
}
export const MetricPickerMenu = ({ options, offset, minWidth, selected, onSelect }: Props): JSX.Element => {
const theme = useTheme2();
const styles = getSelectStyles(theme);
const customStyles = useStyles2(getStyles(minWidth));
return (
<div
className={cx(
styles.menu,
customStyles.menuWrapper,
{ [customStyles.menuLeft]: offset.horizontal > 0 },
css`
bottom: ${offset.vertical > 0 ? `${offset.vertical}px` : 'unset'};
top: ${offset.vertical < 0 ? `${Math.abs(offset.vertical)}px` : 'unset'};
`
)}
>
<div className={customStyles.menu} aria-label="Metric picker menu">
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack>
<div>
<div className={styles.optionBody}>
{options?.map((option, i) => (
<MenuOption data={option} key={i} onClick={onSelect} isFocused={selected === i} hideDescription />
))}
</div>
</div>
</CustomScrollbar>
</div>
</div>
);
};
interface MenuOptionProps<T> {
data: SelectableValue<string>;
onClick: (value: SelectableValue<string>) => void;
isFocused?: boolean;
disabled?: boolean;
hideDescription?: boolean;
}
const MenuOption = React.forwardRef<HTMLDivElement, React.PropsWithChildren<MenuOptionProps<any>>>(
({ data, isFocused, disabled, onClick, hideDescription }, ref) => {
const theme = useTheme2();
const styles = getSelectStyles(theme);
const customStyles = useStyles2(getStyles());
const wrapperClassName = cx(
styles.option,
customStyles.menuOptionWrapper,
isFocused && styles.optionFocused,
disabled && customStyles.menuOptionDisabled
);
const onClickInternal = (event: FormEvent<HTMLElement>) => {
if (disabled) {
return;
}
event.preventDefault();
event.stopPropagation();
onClick(data);
};
return (
<div ref={ref} className={wrapperClassName} aria-label="Menu option" onClick={onClickInternal}>
<div className={cx(styles.optionBody, customStyles.menuOptionBody)}>
<span>{data.label || data.value}</span>
{!hideDescription && data.description && <div className={styles.optionDescription}>{data.description}</div>}
</div>
{data.description && (
<Tooltip content={data.description}>
<Icon name="info-circle" className={customStyles.menuOptionInfoSign} />
</Tooltip>
)}
</div>
);
}
);
MenuOption.displayName = 'MenuOption';
export const getStyles = (menuWidth?: number) => (theme: GrafanaTheme2) => {
return {
menuWrapper: css`
display: flex;
max-height: 650px;
position: absolute;
z-index: ${theme.zIndex.dropdown};
overflow: hidden;
min-width: auto;
`,
menu: css`
min-width: ${theme.spacing(menuWidth || 0)};
& > div {
padding-top: ${theme.spacing(1)};
}
`,
menuLeft: css`
right: 0;
flex-direction: row-reverse;
`,
container: css`
padding: ${theme.spacing(1)};
border: 1px ${theme.colors.border.weak} solid;
border-radius: ${theme.shape.borderRadius(1)};
background-color: ${theme.colors.background.primary};
z-index: ${theme.zIndex.modal};
`,
menuOptionWrapper: css`
padding: ${theme.spacing(0.5)};
`,
menuOptionBody: css`
font-weight: ${theme.typography.fontWeightRegular};
padding: ${theme.spacing(0, 1.5, 0, 0)};
`,
menuOptionDisabled: css`
color: ${theme.colors.text.disabled};
cursor: not-allowed;
`,
menuOptionInfoSign: css`
color: ${theme.colors.text.disabled};
`,
};
};

View File

@@ -0,0 +1,2 @@
export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu
export const METRIC_PICKER_WIDTH = 360;

View File

@@ -4,3 +4,4 @@ export { AckButton } from './AckButton/AckButton';
export { ExploreButton } from './ExploreButton/ExploreButton'; export { ExploreButton } from './ExploreButton/ExploreButton';
export { ExecScriptButton } from './ExecScriptButton/ExecScriptButton'; export { ExecScriptButton } from './ExecScriptButton/ExecScriptButton';
export { ModalController } from './Modal/ModalController'; export { ModalController } from './Modal/ModalController';
export { MetricPicker } from './MetricPicker/MetricPicker';

View File

@@ -1,106 +0,0 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import * as metricFunctions from './metricFunctions';
angular
.module('grafana.directives')
.directive('addMetricFunction',
/** @ngInject */
function($compile) {
var inputTemplate = '<input type="text"'+
' class="gf-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle query-part"' +
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
'<i class="fa fa-plus"></i></a>';
return {
link: function($scope, elem) {
var categories = metricFunctions.getCategories();
var allFunctions = getAllFunctionNames(categories);
$scope.functionMenu = createFunctionDropDownMenu(categories);
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: allFunctions,
minLength: 1,
items: 10,
updater: function (value) {
var funcDef = metricFunctions.getFuncDef(value);
if (!funcDef) {
// try find close match
value = value.toLowerCase();
funcDef = _.find(allFunctions, function(funcName) {
return funcName.toLowerCase().indexOf(value) === 0;
});
if (!funcDef) { return; }
}
$scope.$apply(function() {
$scope.ctrl.addFunction(funcDef);
});
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(function() {
$input.val('');
$input.hide();
$button.show();
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
function getAllFunctionNames(categories) {
return _.reduce(categories, function(list, category) {
_.each(category, function(func) {
list.push(func.name);
});
return list;
}, []);
}
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
return {
text: category,
submenu: _.map(list, function(value) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
};
})
};
});
}

View File

@@ -0,0 +1,195 @@
import _ from 'lodash';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
import { AnnotationQuery, SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
import { ZabbixMetricsQuery } from '../types';
import { ZabbixQueryEditorProps } from './QueryEditor';
import { QueryEditorRow } from './QueryEditor/QueryEditorRow';
import { MetricPicker } from '../../components';
import { getVariableOptions } from './QueryEditor/utils';
import { prepareAnnotation } from '../migrations';
const severityOptions: SelectableValue<number>[] = [
{ 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' },
];
type Props = ZabbixQueryEditorProps & {
annotation?: AnnotationQuery<ZabbixMetricsQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<ZabbixMetricsQuery>) => void;
};
export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasource }: Props) => {
annotation = prepareAnnotation(annotation);
const query = annotation.target;
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: SelectableValue<string>[] = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
const onChange = (query: any) => {
onAnnotationChange({
...annotation,
target: query,
});
};
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
const onTextFilterChange = (prop: string) => {
return (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, [prop]: { filter: newValue } });
}
};
};
const onMinSeverityChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, options: { ...query.options, minSeverity: option.value } });
}
};
const onOptionSwitch = (prop: string) => () => {
onChange({ ...query, options: { ...query.options, [prop]: !query.options[prop] } });
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group?.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host?.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Problem" labelWidth={12}>
<Input
width={24}
defaultValue={query.trigger?.filter}
placeholder="Problem name"
onBlur={onTextFilterChange('trigger')}
/>
</InlineField>
</QueryEditorRow>
<>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.options?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
<InlineField label="Show OK events" labelWidth={24}>
<InlineSwitch value={query.options.showOkEvents} onChange={onOptionSwitch('showOkEvents')} />
</InlineField>
<InlineField label="Hide acknowledged events" labelWidth={24}>
<InlineSwitch value={query.options.hideAcknowledged} onChange={onOptionSwitch('hideAcknowledged')} />
</InlineField>
<InlineField label="Show hostname" labelWidth={24}>
<InlineSwitch value={query.options.showHostname} onChange={onOptionSwitch('showHostname')} />
</InlineField>
</>
</>
);
};

View File

@@ -1,108 +0,0 @@
import React from 'react';
// import rst2html from 'rst2html';
import { FunctionDescriptor, FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls';
// @ts-ignore
import { PopoverController, Popover } from '@grafana/ui';
interface FunctionEditorProps extends FunctionEditorControlsProps {
func: FunctionDescriptor;
}
interface FunctionEditorState {
showingDescription: boolean;
}
class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEditorState> {
private triggerRef = React.createRef<HTMLSpanElement>();
constructor(props: FunctionEditorProps) {
super(props);
this.state = {
showingDescription: false,
};
}
renderContent = ({ updatePopperPosition }) => {
const {
onMoveLeft,
onMoveRight,
func: {
def: { name, description },
},
} = this.props;
const { showingDescription } = this.state;
if (showingDescription) {
return (
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
<h4 style={{ color: 'white' }}> {name} </h4>
<div>{description}</div>
</div>
);
}
return (
<FunctionEditorControls
{...this.props}
onMoveLeft={() => {
onMoveLeft(this.props.func);
updatePopperPosition();
}}
onMoveRight={() => {
onMoveRight(this.props.func);
updatePopperPosition();
}}
onDescriptionShow={() => {
this.setState({ showingDescription: true }, () => {
updatePopperPosition();
});
}}
/>
);
};
render() {
return (
<PopoverController content={this.renderContent} placement="top" hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.triggerRef && (
<Popover
{...popperProps}
referenceElement={this.triggerRef.current}
wrapperClassName="popper"
className="popper__background"
onMouseLeave={() => {
this.setState({ showingDescription: false });
hidePopper();
}}
onMouseEnter={showPopper}
renderArrow={({ arrowProps, placement }) => (
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
)}
/>
)}
<span
ref={this.triggerRef}
onClick={popperProps.show ? hidePopper : showPopper}
onMouseLeave={() => {
hidePopper();
this.setState({ showingDescription: false });
}}
style={{ cursor: 'pointer' }}
>
{this.props.func.def.name}
</span>
</>
);
}}
</PopoverController>
);
}
}
export { FunctionEditor };

View File

@@ -0,0 +1,88 @@
import { css, cx } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import {
Button,
ClickOutsideWrapper,
ContextMenu,
Dropdown,
Icon,
Input,
Menu,
MenuItem,
Portal,
Segment,
useStyles2,
useTheme2,
} from '@grafana/ui';
import { FuncDef } from '../../types';
import { getCategories } from '../../metricFunctions';
// import { mapFuncDefsToSelectables } from './helpers';
type Props = {
// funcDefs: MetricFunc;
onFuncAdd: (def: FuncDef) => void;
};
export function AddZabbixFunction({ onFuncAdd }: Props) {
const [showMenu, setShowMenu] = useState(false);
const styles = useStyles2(getStyles);
const theme = useTheme2();
const onFuncAddInternal = (def: FuncDef) => {
onFuncAdd(def);
setShowMenu(false);
};
const onSearch = (e: React.FormEvent<HTMLInputElement>) => {
console.log(e.currentTarget.value);
};
const onClickOutside = () => {
setShowMenu(false);
};
const menuItems = useMemo(() => buildMenuItems(onFuncAddInternal), [onFuncAdd]);
return (
<div>
{!showMenu && (
<Button
icon="plus"
variant="secondary"
className={cx(styles.button)}
aria-label="Add new function"
onClick={() => setShowMenu(!showMenu)}
/>
)}
{showMenu && (
<ClickOutsideWrapper onClick={onClickOutside} useCapture>
<Input onChange={onSearch} suffix={<Icon name="search" />} />
<Menu style={{ position: 'absolute', zIndex: theme.zIndex.dropdown }}>{menuItems}</Menu>
</ClickOutsideWrapper>
)}
</div>
);
}
function buildMenuItems(onClick: (func: FuncDef) => void) {
const categories = getCategories();
const menuItems: JSX.Element[] = [];
for (const categoryName in categories) {
const functions = categories[categoryName];
const subItems = functions.map((f) => <Menu.Item label={f.name} key={f.name} onClick={() => onClick(f)} />);
menuItems.push(<Menu.Item label={categoryName} key={categoryName} childItems={subItems} />);
}
return menuItems;
}
function getStyles(theme: GrafanaTheme2) {
return {
button: css`
margin-right: ${theme.spacing(0.5)};
`,
};
}

View File

@@ -0,0 +1,50 @@
import { css } from '@emotion/css';
import React from 'react';
import { FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls';
import { useStyles2, Tooltip } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { MetricFunc } from '../../types';
interface FunctionEditorProps extends FunctionEditorControlsProps {
func: MetricFunc;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
icon: css`
margin-right: ${theme.spacing(0.5)};
`,
label: css({
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.bodySmall.fontSize, // to match .gf-form-label
cursor: 'pointer',
display: 'inline-block',
}),
};
};
export const FunctionEditor: React.FC<FunctionEditorProps> = ({ onMoveLeft, onMoveRight, func, ...props }) => {
const styles = useStyles2(getStyles);
const renderContent = ({ updatePopperPosition }: any) => (
<FunctionEditorControls
{...props}
func={func}
onMoveLeft={() => {
onMoveLeft(func);
updatePopperPosition();
}}
onMoveRight={() => {
onMoveRight(func);
updatePopperPosition();
}}
/>
);
return (
<Tooltip content={renderContent} placement="top" interactive>
<span className={styles.label}>{func.def.name}</span>
</Tooltip>
);
};

View File

@@ -0,0 +1,68 @@
import React, { Suspense } from 'react';
import { Icon, Tooltip } from '@grafana/ui';
import { MetricFunc } from '../../types';
const DOCS_FUNC_REF_URL = 'https://alexanderzobnin.github.io/grafana-zabbix/reference/functions/';
export interface FunctionEditorControlsProps {
onMoveLeft: (func: MetricFunc) => void;
onMoveRight: (func: MetricFunc) => void;
onRemove: (func: MetricFunc) => void;
}
const FunctionDescription = React.lazy(async () => {
// @ts-ignore
const { default: rst2html } = await import(/* webpackChunkName: "rst2html" */ 'rst2html');
return {
default(props: { description?: string }) {
return <div dangerouslySetInnerHTML={{ __html: rst2html(props.description ?? '') }} />;
},
};
});
const FunctionHelpButton = (props: { description?: string; name: string }) => {
if (props.description) {
let tooltip = (
<Suspense fallback={<span>Loading description...</span>}>
<FunctionDescription description={props.description} />
</Suspense>
);
return (
<Tooltip content={tooltip} placement={'bottom-end'}>
<Icon className={props.description ? undefined : 'pointer'} name="question-circle" />
</Tooltip>
);
}
return (
<Icon
className="pointer"
name="question-circle"
onClick={() => {
window.open(`${DOCS_FUNC_REF_URL}#${props.name}`, '_blank');
}}
/>
);
};
export const FunctionEditorControls = (
props: FunctionEditorControlsProps & {
func: MetricFunc;
}
) => {
const { func, onMoveLeft, onMoveRight, onRemove } = props;
return (
<div
style={{
display: 'flex',
width: '60px',
justifyContent: 'space-between',
}}
>
<Icon className="pointer" name="arrow-left" onClick={() => onMoveLeft(func)} />
<FunctionHelpButton name={func.def.name} description={func.def.description} />
<Icon className="pointer" name="times" onClick={() => onRemove(func)} />
<Icon className="pointer" name="arrow-right" onClick={() => onMoveRight(func)} />
</div>
);
};

View File

@@ -0,0 +1,77 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Segment, SegmentInput, useStyles2 } from '@grafana/ui';
export type EditableParam = {
name: string;
value: string;
optional: boolean;
multiple: boolean;
options: Array<SelectableValue<string>>;
};
type FieldEditorProps = {
editableParam: EditableParam;
onChange: (value: string) => void;
onExpandedChange: (expanded: boolean) => void;
autofocus: boolean;
};
/**
* Render a function parameter with a segment dropdown for multiple options or simple input.
*/
export function FunctionParamEditor({ editableParam, onChange, onExpandedChange, autofocus }: FieldEditorProps) {
const styles = useStyles2(getStyles);
if (editableParam.options?.length > 0) {
return (
<Segment
autofocus={autofocus}
value={editableParam.value}
inputPlaceholder={editableParam.name}
className={styles.segment}
options={editableParam.options}
placeholder={' +' + editableParam.name}
onChange={(value) => {
onChange(value.value || '');
}}
onExpandedChange={onExpandedChange}
inputMinWidth={150}
allowCustomValue={true}
allowEmptyValue={true}
></Segment>
);
} else {
return (
<SegmentInput
autofocus={autofocus}
className={styles.input}
value={editableParam.value || ''}
placeholder={' +' + editableParam.name}
inputPlaceholder={editableParam.name}
onChange={(value) => {
onChange(value.toString());
}}
onExpandedChange={onExpandedChange}
// input style
style={{ height: '25px', paddingTop: '2px', marginTop: '2px', paddingLeft: '4px', minWidth: '100px' }}
></SegmentInput>
);
}
}
const getStyles = (theme: GrafanaTheme2) => ({
segment: css({
margin: 0,
padding: 0,
}),
input: css`
margin: 0;
padding: 0;
input {
height: 25px;
},
`,
});

View File

@@ -0,0 +1,90 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup, InlineLabel, useStyles2 } from '@grafana/ui';
import { FunctionEditor } from './FunctionEditor';
import { EditableParam, FunctionParamEditor } from './FunctionParamEditor';
import { mapFuncInstanceToParams } from './helpers';
import { MetricFunc } from '../../types';
export type FunctionEditorProps = {
func: MetricFunc;
onMoveLeft: (func: MetricFunc) => void;
onMoveRight: (func: MetricFunc) => void;
onRemove: (func: MetricFunc) => void;
onParamChange: (func: MetricFunc, index: number, value: string) => void;
};
/**
* Allows editing function params and removing/moving a function (note: editing function name is not supported)
*/
export function ZabbixFunctionEditor({ func, onMoveLeft, onMoveRight, onRemove, onParamChange }: FunctionEditorProps) {
const styles = useStyles2(getStyles);
// keep track of mouse over and isExpanded state to display buttons for adding optional/multiple params
// only when the user mouse over over the function editor OR any param editor is expanded.
const [mouseOver, setIsMouseOver] = useState(false);
const [expanded, setIsExpanded] = useState(false);
let params = mapFuncInstanceToParams(func);
params = params.filter((p: EditableParam, index: number) => {
// func.added is set for newly added functions - see autofocus below
return (index < func.def.params.length && !p.optional) || func.added || p.value || expanded || mouseOver;
});
return (
<div
className={cx(styles.container)}
onMouseOver={() => setIsMouseOver(true)}
onMouseLeave={() => setIsMouseOver(false)}
>
<HorizontalGroup spacing="none">
<FunctionEditor func={func} onMoveLeft={onMoveLeft} onMoveRight={onMoveRight} onRemove={onRemove} />
<InlineLabel className={styles.label}>(</InlineLabel>
{params.map((editableParam: EditableParam, index: number) => {
return (
<React.Fragment key={index}>
<FunctionParamEditor
autofocus={index === 0 && func.added}
editableParam={editableParam}
onChange={(value) => {
if (value !== '' || editableParam.optional) {
// dispatch(actions.updateFunctionParam({ func, index, value }));
onParamChange(func, index, value);
}
setIsExpanded(false);
setIsMouseOver(false);
}}
onExpandedChange={setIsExpanded}
/>
{index !== params.length - 1 ? ',' : ''}
</React.Fragment>
);
})}
<InlineLabel className={styles.label}>)</InlineLabel>
</HorizontalGroup>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(),
marginRight: theme.spacing(0.5),
padding: `0 ${theme.spacing(1)}`,
height: `${theme.v1.spacing.formInputHeight}px`,
}),
error: css`
border: 1px solid ${theme.colors.error.main};
`,
label: css({
padding: 0,
margin: 0,
}),
button: css({
padding: theme.spacing(0.5),
}),
});

View File

@@ -0,0 +1,58 @@
import { SelectableValue } from '@grafana/data';
import { MetricFunc } from '../../types';
export type ParamDef = {
name: string;
type: string;
options?: Array<string | number>;
multiple?: boolean;
optional?: boolean;
version?: string;
};
export type EditableParam = {
name: string;
value: string;
optional: boolean;
multiple: boolean;
options: Array<SelectableValue<string>>;
};
function createEditableParam(paramDef: ParamDef, additional: boolean, value?: string | number): EditableParam {
return {
name: paramDef.name,
value: value?.toString() || '',
optional: !!paramDef.optional || additional, // only first param is required when multiple are allowed
multiple: !!paramDef.multiple,
options:
paramDef.options?.map((option: string | number) => ({
value: option.toString(),
label: option.toString(),
})) ?? [],
};
}
/**
* Create a list of params that can be edited in the function editor.
*/
export function mapFuncInstanceToParams(func: MetricFunc): EditableParam[] {
// list of required parameters (from func.def)
const params: EditableParam[] = func.def.params.map((paramDef: ParamDef, index: number) =>
createEditableParam(paramDef, false, func.params[index])
);
// list of additional (multiple or optional) params entered by the user
while (params.length < func.params.length) {
const paramDef = func.def.params[func.def.params.length - 1];
const value = func.params[params.length];
params.push(createEditableParam(paramDef, true, value));
}
// extra "fake" param to allow adding more multiple values at the end
if (params.length && params[params.length - 1].value && params[params.length - 1]?.multiple) {
const paramDef = func.def.params[func.def.params.length - 1];
params.push(createEditableParam(paramDef, true, ''));
}
return params;
}

View File

@@ -1,67 +0,0 @@
import React from 'react';
const DOCS_FUNC_REF_URL = 'https://alexanderzobnin.github.io/grafana-zabbix/reference/functions/';
export interface FunctionDescriptor {
text: string;
params: string[];
def: {
category: string;
defaultParams: string[];
description?: string;
fake: boolean;
name: string;
params: string[];
};
}
export interface FunctionEditorControlsProps {
onMoveLeft: (func: FunctionDescriptor) => void;
onMoveRight: (func: FunctionDescriptor) => void;
onRemove: (func: FunctionDescriptor) => void;
}
const FunctionHelpButton = (props: { description: string; name: string; onDescriptionShow: () => void }) => {
if (props.description) {
return <span className="pointer fa fa-question-circle" onClick={props.onDescriptionShow} />;
}
return (
<span
className="pointer fa fa-question-circle"
onClick={() => {
window.open(
DOCS_FUNC_REF_URL + '#' + props.name,
'_blank'
);
}}
/>
);
};
export const FunctionEditorControls = (
props: FunctionEditorControlsProps & {
func: FunctionDescriptor;
onDescriptionShow: () => void;
}
) => {
const { func, onMoveLeft, onMoveRight, onRemove, onDescriptionShow } = props;
return (
<div
style={{
display: 'flex',
width: '60px',
justifyContent: 'space-between',
}}
>
<span className="pointer fa fa-arrow-left" onClick={() => onMoveLeft(func)} />
<FunctionHelpButton
name={func.def.name}
description={func.def.description}
onDescriptionShow={onDescriptionShow}
/>
<span className="pointer fa fa-remove" onClick={() => onRemove(func)} />
<span className="pointer fa fa-arrow-right" onClick={() => onMoveRight(func)} />
</div>
);
};

View File

@@ -0,0 +1,212 @@
import React, { useEffect } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
import * as c from '../constants';
import * as migrations from '../migrations';
import { ZabbixDatasource } from '../datasource';
import { MetricFunc, ShowProblemTypes, ZabbixDSOptions, ZabbixMetricsQuery, ZabbixQueryOptions } from '../types';
import { MetricsQueryEditor } from './QueryEditor/MetricsQueryEditor';
import { QueryFunctionsEditor } from './QueryEditor/QueryFunctionsEditor';
import { QueryOptionsEditor } from './QueryEditor/QueryOptionsEditor';
import { TextMetricsQueryEditor } from './QueryEditor/TextMetricsQueryEditor';
import { ProblemsQueryEditor } from './QueryEditor/ProblemsQueryEditor';
import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor';
import { ITServicesQueryEditor } from './QueryEditor/ITServicesQueryEditor';
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
{
value: c.MODE_METRICS,
label: 'Metrics',
description: 'Query numeric metrics',
},
{
value: c.MODE_TEXT,
label: 'Text',
description: 'Query text data',
},
{
value: c.MODE_ITSERVICE,
label: 'IT Services',
description: 'Query IT Services data',
},
{
value: c.MODE_ITEMID,
label: 'Item Id',
description: 'Query metrics by item ids',
},
{
value: c.MODE_TRIGGERS,
label: 'Triggers',
description: 'Query triggers data',
},
{
value: c.MODE_PROBLEMS,
label: 'Problems',
description: 'Query problems',
},
];
const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
queryType: c.MODE_METRICS,
group: { filter: '' },
host: { filter: '' },
application: { filter: '' },
itemTag: { filter: '' },
item: { filter: '' },
functions: [],
triggers: {
count: true,
minSeverity: 3,
acknowledged: 2,
},
trigger: { filter: '' },
tags: { filter: '' },
proxy: { filter: '' },
textFilter: '',
options: {
showDisabledItems: false,
skipEmptyValues: false,
disableDataAlignment: false,
useZabbixValueMapping: false,
},
table: {
skipEmptyValues: false,
},
});
function getSLAQueryDefaults() {
return {
itServiceFilter: '',
slaProperty: 'sla',
slaInterval: 'none',
};
}
function getProblemsQueryDefaults(): Partial<ZabbixMetricsQuery> {
return {
showProblems: ShowProblemTypes.Problems,
options: {
minSeverity: 0,
sortProblems: 'default',
acknowledged: 2,
hostsInMaintenance: false,
hostProxy: false,
limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT,
useTimeRange: false,
},
};
}
export interface ZabbixQueryEditorProps
extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {}
export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQueryEditorProps) => {
query = { ...getDefaultQuery(), ...query };
const { queryType } = query;
if (queryType === c.MODE_PROBLEMS || queryType === c.MODE_TRIGGERS) {
const defaults = getProblemsQueryDefaults();
query = { ...defaults, ...query };
query.options = { ...defaults.options, ...query.options };
}
if (queryType === c.MODE_ITSERVICE) {
query = { ...getSLAQueryDefaults(), ...query };
}
// Migrate query on load
useEffect(() => {
const migratedQuery = migrations.migrate(query);
onChange(migratedQuery);
}, []);
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value !== null) {
onChangeInternal({ ...query, [prop]: option.value });
}
};
};
const onChangeInternal = (query: ZabbixMetricsQuery) => {
onChange(query);
onRunQuery();
};
const onOptionsChange = (options: ZabbixQueryOptions) => {
onChangeInternal({ ...query, options });
};
const getSelectableValue = (value: string): SelectableValue<string> => {
return { value, label: value };
};
const renderMetricsEditor = () => {
return (
<>
<MetricsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</>
);
};
const renderItemIdsEditor = () => {
return (
<>
<ItemIdQueryEditor query={query} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</>
);
};
const renderTextMetricsEditor = () => {
return (
<>
<TextMetricsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
{/* <QueryFunctionsEditor query={query} onChange={onChangeInternal} /> */}
</>
);
};
const renderITServicesEditor = () => {
return (
<>
<ITServicesQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</>
);
};
const renderProblemsEditor = () => {
return <ProblemsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
};
const renderTriggersEditor = () => {
return <TriggersQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
};
return (
<>
<InlineFieldRow>
<InlineField label="Query type" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={queryType}
options={zabbixQueryTypeOptions}
onChange={onPropChange('queryType')}
/>
</InlineField>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</InlineFieldRow>
{queryType === c.MODE_METRICS && renderMetricsEditor()}
{queryType === c.MODE_ITEMID && renderItemIdsEditor()}
{queryType === c.MODE_TEXT && renderTextMetricsEditor()}
{queryType === c.MODE_ITSERVICE && renderITServicesEditor()}
{queryType === c.MODE_PROBLEMS && renderProblemsEditor()}
{queryType === c.MODE_TRIGGERS && renderTriggersEditor()}
<QueryOptionsEditor queryType={queryType} queryOptions={query.options} onChange={onOptionsChange} />
</>
);
};

View File

@@ -0,0 +1,102 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const slaPropertyList: SelectableValue<string>[] = [
{ label: 'Status', value: 'status' },
{ label: 'SLA', value: 'sla' },
{ label: 'OK time', value: 'okTime' },
{ label: 'Problem time', value: 'problemTime' },
{ label: 'Down time', value: 'downtimeTime' },
];
const slaIntervals: SelectableValue<string>[] = [
{ label: 'No interval', value: 'none' },
{ label: 'Auto', value: 'auto' },
{ label: '1 hour', value: '1h' },
{ label: '12 hours', value: '12h' },
{ label: '24 hours', value: '1d' },
{ label: '1 week', value: '1w' },
{ label: '1 month', value: '1M' },
];
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ITServicesQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadITServiceOptions = async () => {
const services = await datasource.zabbix.getITService();
const options = services?.map((s) => ({
value: s.name,
label: s.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: itServicesLoading, value: itServicesOptions }, fetchITServices] = useAsyncFn(async () => {
const options = await loadITServiceOptions();
return options;
}, []);
useEffect(() => {
fetchITServices();
}, []);
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value) {
onChange({ ...query, [prop]: option.value });
}
};
};
const onITServiceChange = (value: string) => {
if (value !== null) {
onChange({ ...query, itServiceFilter: value });
}
};
return (
<QueryEditorRow>
<InlineField label="IT Service" labelWidth={12}>
<MetricPicker
width={24}
value={query.itServiceFilter}
options={itServicesOptions}
isLoading={itServicesLoading}
onChange={onITServiceChange}
/>
</InlineField>
<InlineField label="Property" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaProperty}
options={slaPropertyList}
onChange={onPropChange('slaProperty')}
/>
</InlineField>
<InlineField label="Interval" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaInterval}
options={slaIntervals}
onChange={onPropChange('slaInterval')}
/>
</InlineField>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,26 @@
import React, { FormEvent } from 'react';
import { InlineField, Input } from '@grafana/ui';
import { ZabbixMetricsQuery } from '../../types';
import { QueryEditorRow } from './QueryEditorRow';
export interface Props {
query: ZabbixMetricsQuery;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ItemIdQueryEditor = ({ query, onChange }: Props) => {
const onItemIdsChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, itemids: newValue });
}
};
return (
<QueryEditorRow>
<InlineField label="Item Ids" labelWidth={12}>
<Input width={24} defaultValue={query.itemids} onBlur={onItemIdsChange} />
</InlineField>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,174 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: SelectableValue<string>[] = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const appFilter = datasource.replaceTemplateVars(app);
const tagFilter = datasource.replaceTemplateVars(itemTag);
const options = {
itemtype: 'num',
showDisabledItems: query.options.showDisabledItems,
};
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
let itemOptions: SelectableValue<string>[] = items?.map((item) => ({
value: item.name,
label: item.name,
}));
itemOptions = _.uniqBy(itemOptions, (o) => o.value);
itemOptions.unshift(...getVariableOptions());
return itemOptions;
};
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions(
query.group.filter,
query.host.filter,
query.application.filter,
query.itemTag.filter
);
return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchItems();
}, [groupFilter, hostFilter, appFilter, tagFilter]);
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Item" labelWidth={12}>
<MetricPicker
width={24}
value={query.item.filter}
options={itemOptions}
isLoading={itemsLoading}
onChange={onFilterChange('item')}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,232 @@
import _ from 'lodash';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, Input, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const showProblemsOptions: SelectableValue<string>[] = [
{ label: 'Problems', value: 'problems' },
{ label: 'Recent problems', value: 'recent' },
{ label: 'History', value: 'history' },
];
const severityOptions: SelectableValue<number>[] = [
{ 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 interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: SelectableValue<string>[] = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
const loadProxyOptions = async () => {
const proxies = await datasource.zabbix.getProxies();
const options = proxies?.map((proxy) => ({
value: proxy.host,
label: proxy.host,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: proxiesLoading, value: proxiesOptions }, fetchProxies] = useAsyncFn(async () => {
const options = await loadProxyOptions();
return options;
}, []);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchProxies();
}, []);
const onTextFilterChange = (prop: string) => {
return (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, [prop]: { filter: newValue } });
}
};
};
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, [prop]: option.value });
}
};
};
const onMinSeverityChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, options: { ...query.options, minSeverity: option.value } });
}
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group?.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host?.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
<InlineField label="Proxy" labelWidth={12}>
<MetricPicker
width={24}
value={query.proxy?.filter}
options={proxiesOptions}
isLoading={proxiesLoading}
onChange={onFilterChange('proxy')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Problem" labelWidth={12}>
<Input
width={24}
defaultValue={query.trigger?.filter}
placeholder="Problem name"
onBlur={onTextFilterChange('trigger')}
/>
</InlineField>
<InlineField label="Tags" labelWidth={12}>
<Input
width={24}
defaultValue={query.tags?.filter}
placeholder="tag1:value1, tag2:value2"
onBlur={onTextFilterChange('tags')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Show" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.showProblems}
options={showProblemsOptions}
onChange={onPropChange('showProblems')}
/>
</InlineField>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.options?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { InlineFieldRow } from '@grafana/ui';
export const QueryEditorRow = ({ children }: React.PropsWithChildren<{}>) => {
return (
<InlineFieldRow>
{children}
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</InlineFieldRow>
);
};

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { swap } from '../../utils';
import { createFuncInstance } from '../../metricFunctions';
import { FuncDef, MetricFunc, ZabbixMetricsQuery } from '../../types';
import { QueryEditorRow } from './QueryEditorRow';
import { InlineFormLabel } from '@grafana/ui';
import { ZabbixFunctionEditor } from '../FunctionEditor/ZabbixFunctionEditor';
import { AddZabbixFunction } from '../FunctionEditor/AddZabbixFunction';
export interface Props {
query: ZabbixMetricsQuery;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const QueryFunctionsEditor = ({ query, onChange }: Props) => {
const onFuncParamChange = (func: MetricFunc, index: number, value: string) => {
func.params[index] = value;
const funcIndex = query.functions.findIndex((f) => f === func);
const functions = query.functions;
functions[funcIndex] = func;
onChange({ ...query, functions });
};
const onMoveFuncLeft = (func: MetricFunc) => {
const index = query.functions.indexOf(func);
const functions = swap(query.functions, index, index - 1);
onChange({ ...query, functions });
};
const onMoveFuncRight = (func: MetricFunc) => {
const index = query.functions.indexOf(func);
const functions = swap(query.functions, index, index + 1);
onChange({ ...query, functions });
};
const onRemoveFunc = (func: MetricFunc) => {
const functions = query.functions?.filter((f) => f != func);
onChange({ ...query, functions });
};
const onFuncAdd = (def: FuncDef) => {
const newFunc = createFuncInstance(def);
newFunc.added = true;
let functions = query.functions.concat(newFunc);
functions = moveAliasFuncLast(functions);
// if ((newFunc.params.length && newFunc.added) || newFunc.def.params.length === 0) {
// }
onChange({ ...query, functions });
};
return (
<QueryEditorRow>
<InlineFormLabel width={6}>Functions</InlineFormLabel>
{query.functions?.map((f, i) => {
return (
<ZabbixFunctionEditor
func={f}
key={i}
onParamChange={onFuncParamChange}
onMoveLeft={onMoveFuncLeft}
onMoveRight={onMoveFuncRight}
onRemove={onRemoveFunc}
/>
);
})}
<AddZabbixFunction onFuncAdd={onFuncAdd} />
</QueryEditorRow>
);
};
function moveAliasFuncLast(functions: MetricFunc[]) {
const aliasFuncIndex = functions.findIndex((func) => func.def.category === 'Alias');
console.log(aliasFuncIndex);
if (aliasFuncIndex >= 0) {
const aliasFunc = functions[aliasFuncIndex];
functions.splice(aliasFuncIndex, 1);
functions.push(aliasFunc);
}
return functions;
}

View File

@@ -0,0 +1,232 @@
import { css } from '@emotion/css';
import React, { useState, FormEvent } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import {
HorizontalGroup,
Icon,
InlineField,
InlineFieldRow,
InlineSwitch,
Input,
Select,
useStyles2,
} from '@grafana/ui';
import * as c from '../../constants';
import { ZabbixQueryOptions } from '../../types';
const ackOptions: SelectableValue<number>[] = [
{ label: 'all triggers', value: 2 },
{ label: 'unacknowledged', value: 0 },
{ label: 'acknowledged', value: 1 },
];
const sortOptions: SelectableValue<string>[] = [
{ label: 'Default', value: 'default' },
{ label: 'Last change', value: 'lastchange' },
{ label: 'Severity', value: 'severity' },
];
interface Props {
queryType: string;
queryOptions: ZabbixQueryOptions;
onChange: (options: ZabbixQueryOptions) => void;
}
export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const styles = useStyles2(getStyles);
const onLimitChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = Number(v?.currentTarget?.value);
if (newValue !== null) {
onChange({ ...queryOptions, limit: newValue });
}
};
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...queryOptions, [prop]: option.value });
}
};
};
const renderClosed = () => {
return (
<>
<HorizontalGroup>
{!isOpen && <Icon name="angle-right" />}
{isOpen && <Icon name="angle-down" />}
<span className={styles.label}>Options</span>
<div className={styles.options}>{renderOptions()}</div>
</HorizontalGroup>
</>
);
};
const renderOptions = () => {
const elements: JSX.Element[] = [];
for (const key in queryOptions) {
if (queryOptions.hasOwnProperty(key)) {
const value = queryOptions[key];
if (value === true && value !== '' && value !== null && value !== undefined) {
elements.push(<span className={styles.optionContainer} key={key}>{`${key} = ${value}`}</span>);
}
}
}
return elements;
};
const renderEditor = () => {
return (
<div className={styles.editorContainer}>
{queryType === c.MODE_METRICS && renderMetricOptions()}
{queryType === c.MODE_ITEMID && renderMetricOptions()}
{queryType === c.MODE_ITSERVICE && renderMetricOptions()}
{queryType === c.MODE_TEXT && renderTextMetricsOptions()}
{queryType === c.MODE_PROBLEMS && renderProblemsOptions()}
{queryType === c.MODE_TRIGGERS && renderTriggersOptions()}
</div>
);
};
const renderMetricOptions = () => {
return (
<>
<InlineField label="Show disabled items" labelWidth={24}>
<InlineSwitch
value={queryOptions.showDisabledItems}
onChange={() => onChange({ ...queryOptions, showDisabledItems: !queryOptions.showDisabledItems })}
/>
</InlineField>
<InlineField label="Use Zabbix value mapping" labelWidth={24}>
<InlineSwitch
value={queryOptions.useZabbixValueMapping}
onChange={() => onChange({ ...queryOptions, useZabbixValueMapping: !queryOptions.useZabbixValueMapping })}
/>
</InlineField>
<InlineField label="Disable data alignment" labelWidth={24}>
<InlineSwitch
value={queryOptions.disableDataAlignment}
onChange={() => onChange({ ...queryOptions, disableDataAlignment: !queryOptions.disableDataAlignment })}
/>
</InlineField>
</>
);
};
const renderTextMetricsOptions = () => {
return (
<>
<InlineField label="Show disabled items" labelWidth={24}>
<InlineSwitch
value={queryOptions.showDisabledItems}
onChange={() => onChange({ ...queryOptions, showDisabledItems: !queryOptions.showDisabledItems })}
/>
</InlineField>
</>
);
};
const renderProblemsOptions = () => {
return (
<>
<InlineField label="Acknowledged" labelWidth={24}>
<Select
isSearchable={false}
width={24}
value={queryOptions.acknowledged}
options={ackOptions}
onChange={onPropChange('acknowledged')}
/>
</InlineField>
<InlineField label="Sort by" labelWidth={24}>
<Select
isSearchable={false}
width={24}
value={queryOptions.sortProblems}
options={sortOptions}
onChange={onPropChange('sortProblems')}
/>
</InlineField>
<InlineField label="Use time range" labelWidth={24}>
<InlineSwitch
value={queryOptions.useTimeRange}
onChange={() => onChange({ ...queryOptions, useTimeRange: !queryOptions.useTimeRange })}
/>
</InlineField>
<InlineField label="Hosts in maintenance" labelWidth={24}>
<InlineSwitch
value={queryOptions.hostsInMaintenance}
onChange={() => onChange({ ...queryOptions, hostsInMaintenance: !queryOptions.hostsInMaintenance })}
/>
</InlineField>
<InlineField label="Host proxy" labelWidth={24}>
<InlineSwitch
value={queryOptions.hostProxy}
onChange={() => onChange({ ...queryOptions, hostProxy: !queryOptions.hostProxy })}
/>
</InlineField>
<InlineField label="Limit" labelWidth={24}>
<Input width={12} type="number" defaultValue={queryOptions.limit} onBlur={onLimitChange} />
</InlineField>
</>
);
};
const renderTriggersOptions = () => {
return (
<>
<InlineField label="Acknowledged" labelWidth={24}>
<Select
isSearchable={false}
width={24}
value={queryOptions.acknowledged}
options={ackOptions}
onChange={onPropChange('acknowledged')}
/>
</InlineField>
</>
);
};
return (
<>
<InlineFieldRow>
<div className={styles.container} onClick={() => setIsOpen(!isOpen)}>
{renderClosed()}
</div>
</InlineFieldRow>
<InlineFieldRow>{isOpen && renderEditor()}</InlineFieldRow>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(),
marginRight: theme.spacing(0.5),
marginBottom: theme.spacing(0.5),
padding: `0 ${theme.spacing(1)}`,
height: `${theme.v1.spacing.formInputHeight}px`,
width: `100%`,
}),
label: css({
color: theme.colors.info.text,
fontWeight: theme.typography.fontWeightMedium,
cursor: 'pointer',
}),
options: css({
color: theme.colors.text.disabled,
fontSize: theme.typography.bodySmall.fontSize,
}),
optionContainer: css`
margin-right: ${theme.spacing(2)};
`,
editorContainer: css`
display: flex;
flex-direction: column;
margin-left: ${theme.spacing(4)};
`,
});

View File

@@ -0,0 +1,192 @@
import _ from 'lodash';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Input } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: SelectableValue<string>[] = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const appFilter = datasource.replaceTemplateVars(app);
const tagFilter = datasource.replaceTemplateVars(itemTag);
const options = {
itemtype: 'text',
showDisabledItems: query.options.showDisabledItems,
};
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
let itemOptions: SelectableValue<string>[] = items?.map((item) => ({
value: item.name,
label: item.name,
}));
itemOptions = _.uniqBy(itemOptions, (o) => o.value);
itemOptions.unshift(...getVariableOptions());
return itemOptions;
};
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions(
query.group.filter,
query.host.filter,
query.application.filter,
query.itemTag.filter
);
return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchItems();
}, [groupFilter, hostFilter, appFilter, tagFilter]);
const onTextFilterChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, textFilter: newValue });
}
};
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Item" labelWidth={12}>
<MetricPicker
width={24}
value={query.item.filter}
options={itemOptions}
isLoading={itemsLoading}
onChange={onFilterChange('item')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Text filter" labelWidth={12}>
<Input width={24} defaultValue={query.textFilter} onBlur={onTextFilterChange} />
</InlineField>
<InlineField label="Use capture groups" labelWidth={16}>
<InlineSwitch
value={query.useCaptureGroups}
onChange={() => onChange({ ...query, useCaptureGroups: !query.useCaptureGroups })}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,160 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const severityOptions: SelectableValue<number>[] = [
{ 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 interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: SelectableValue<string>[] = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
const onMinSeverityChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, options: { ...query.options, minSeverity: option.value } });
}
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group?.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host?.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.triggers?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
<InlineField label="Count" labelWidth={12}>
<InlineSwitch
value={query.triggers?.count}
onChange={() => onChange({ ...query, triggers: { ...query.triggers, count: !query.triggers?.count } })}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,13 @@
import { getTemplateSrv } from '@grafana/runtime';
export const getVariableOptions = () => {
const variables = getTemplateSrv()
.getVariables()
.filter((v) => {
return v.type !== 'datasource' && v.type !== 'interval';
});
return variables?.map((v) => ({
value: `$${v.name}`,
label: `$${v.name}`,
}));
};

View File

@@ -35,10 +35,10 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
this.state = { this.state = {
selectedQueryType, selectedQueryType,
legacyQuery: this.props.query, legacyQuery: this.props.query,
...query ...query,
}; };
} else if (this.props.query) { } else if (this.props.query) {
const query = (this.props.query as VariableQuery); const query = this.props.query as VariableQuery;
const selectedQueryType = this.getSelectedQueryType(query.queryType); const selectedQueryType = this.getSelectedQueryType(query.queryType);
this.state = { this.state = {
...this.defaults, ...this.defaults,
@@ -51,7 +51,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
} }
getSelectedQueryType(queryType: VariableQueryTypes) { getSelectedQueryType(queryType: VariableQueryTypes) {
return this.queryTypes.find(q => q.value === queryType); return this.queryTypes.find((q) => q.value === queryType);
} }
handleQueryUpdate = (evt: React.ChangeEvent<HTMLInputElement>, prop: string) => { handleQueryUpdate = (evt: React.ChangeEvent<HTMLInputElement>, prop: string) => {
@@ -108,70 +108,68 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
<InlineFormLabel width={10}>Group</InlineFormLabel> <InlineFormLabel width={10}>Group</InlineFormLabel>
<ZabbixInput <ZabbixInput
value={group} value={group}
onChange={evt => this.handleQueryUpdate(evt, 'group')} onChange={(evt) => this.handleQueryUpdate(evt, 'group')}
onBlur={this.handleQueryChange} onBlur={this.handleQueryChange}
/> />
</div> </div>
{selectedQueryType.value !== VariableQueryTypes.Group && {selectedQueryType.value !== VariableQueryTypes.Group && (
<div className="gf-form max-width-30"> <div className="gf-form max-width-30">
<InlineFormLabel width={10}>Host</InlineFormLabel> <InlineFormLabel width={10}>Host</InlineFormLabel>
<ZabbixInput <ZabbixInput
value={host} value={host}
onChange={evt => this.handleQueryUpdate(evt, 'host')} onChange={(evt) => this.handleQueryUpdate(evt, 'host')}
onBlur={this.handleQueryChange} onBlur={this.handleQueryChange}
/> />
</div> </div>
} )}
</div> </div>
{(selectedQueryType.value === VariableQueryTypes.Application || {(selectedQueryType.value === VariableQueryTypes.Application ||
selectedQueryType.value === VariableQueryTypes.ItemTag || selectedQueryType.value === VariableQueryTypes.ItemTag ||
selectedQueryType.value === VariableQueryTypes.Item || selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) && selectedQueryType.value === VariableQueryTypes.ItemValues) && (
<div className="gf-form-inline"> <div className="gf-form-inline">
{supportsItemTags && ( {supportsItemTags && (
<div className="gf-form max-width-30"> <div className="gf-form max-width-30">
<InlineFormLabel width={10}>Item tag</InlineFormLabel> <InlineFormLabel width={10}>Item tag</InlineFormLabel>
<ZabbixInput <ZabbixInput
value={itemTag} value={itemTag}
onChange={evt => this.handleQueryUpdate(evt, 'itemTag')} onChange={(evt) => this.handleQueryUpdate(evt, 'itemTag')}
onBlur={this.handleQueryChange} onBlur={this.handleQueryChange}
/> />
</div> </div>
)} )}
{!supportsItemTags && ( {!supportsItemTags && (
<div className="gf-form max-width-30"> <div className="gf-form max-width-30">
<InlineFormLabel width={10}>Application</InlineFormLabel> <InlineFormLabel width={10}>Application</InlineFormLabel>
<ZabbixInput <ZabbixInput
value={application} value={application}
onChange={evt => this.handleQueryUpdate(evt, 'application')} onChange={(evt) => this.handleQueryUpdate(evt, 'application')}
onBlur={this.handleQueryChange} onBlur={this.handleQueryChange}
/> />
</div> </div>
)} )}
{(selectedQueryType.value === VariableQueryTypes.Item || {(selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) && selectedQueryType.value === VariableQueryTypes.ItemValues) && (
<div className="gf-form max-width-30"> <div className="gf-form max-width-30">
<InlineFormLabel width={10}>Item</InlineFormLabel> <InlineFormLabel width={10}>Item</InlineFormLabel>
<ZabbixInput <ZabbixInput
value={item} value={item}
onChange={evt => this.handleQueryUpdate(evt, 'item')} onChange={(evt) => this.handleQueryUpdate(evt, 'item')}
onBlur={this.handleQueryChange} onBlur={this.handleQueryChange}
/> />
</div>
)}
</div> </div>
} )}
</div>
}
{legacyQuery && {legacyQuery && (
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel width={10} tooltip="Original query string, read-only">Legacy Query</InlineFormLabel> <InlineFormLabel width={10} tooltip="Original query string, read-only">
<Input Legacy Query
css="" </InlineFormLabel>
value={legacyQuery} <Input value={legacyQuery} readOnly={true} />
readOnly={true} </div>
/> )}
</div>
}
</> </>
); );
} }

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { css, cx } from '@emotion/css'; import { css } from '@emotion/css';
import { EventsWithValidation, ValidationEvents, useTheme } from '@grafana/ui'; import { EventsWithValidation, ValidationEvents, useStyles2 } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { isRegex, variableRegex } from '../utils'; import { isRegex, variableRegex } from '../utils';
import * as grafanaUi from '@grafana/ui'; import * as grafanaUi from '@grafana/ui';
@@ -9,19 +9,19 @@ const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
const variablePattern = RegExp(`^${variableRegex.source}`); const variablePattern = RegExp(`^${variableRegex.source}`);
const getStyles = (theme: GrafanaTheme) => ({ const getStyles = (theme: GrafanaTheme2) => ({
inputRegex: css` inputRegex: css`
color: ${theme.palette.orange} color: ${theme.colors.warning.main};
`, `,
inputVariable: css` inputVariable: css`
color: ${theme.colors.textBlue} color: ${theme.colors.action.focus};
`, `,
}); });
const zabbixInputValidationEvents: ValidationEvents = { const zabbixInputValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [ [EventsWithValidation.onBlur]: [
{ {
rule: value => { rule: (value) => {
if (!value) { if (!value) {
return true; return true;
} }
@@ -35,7 +35,7 @@ const zabbixInputValidationEvents: ValidationEvents = {
errorMessage: 'Not a valid regex', errorMessage: 'Not a valid regex',
}, },
{ {
rule: value => { rule: (value) => {
if (value === '*') { if (value === '*') {
return false; return false;
} }
@@ -47,8 +47,7 @@ const zabbixInputValidationEvents: ValidationEvents = {
}; };
export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => { export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => {
const theme = useTheme(); const styles = useStyles2(getStyles);
const styles = getStyles(theme);
let inputClass = styles.inputRegex; let inputClass = styles.inputRegex;
if (variablePattern.test(value as string)) { if (variablePattern.test(value as string)) {
@@ -57,12 +56,5 @@ export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProp
inputClass = styles.inputRegex; inputClass = styles.inputRegex;
} }
return ( return <Input className={inputClass} value={value} validationEvents={zabbixInputValidationEvents} {...restProps} />;
<Input
className={inputClass}
value={value}
validationEvents={zabbixInputValidationEvents}
{...restProps}
/>
);
}; };

View File

@@ -22,8 +22,10 @@ import {
DataSourceInstanceSettings, DataSourceInstanceSettings,
FieldType, FieldType,
isDataFrame, isDataFrame,
LoadingState LoadingState,
toDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> { export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> {
name: string; name: string;
@@ -53,6 +55,11 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
this.enableDebugLog = config.buildInfo.env === 'development'; this.enableDebugLog = config.buildInfo.env === 'development';
this.annotations = {
QueryEditor: AnnotationQueryEditor,
prepareAnnotation: migrations.prepareAnnotation,
};
// Use custom format for template variables // Use custom format for template variables
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv); this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
@@ -108,7 +115,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
*/ */
query(request: DataQueryRequest<ZabbixMetricsQuery>) { query(request: DataQueryRequest<ZabbixMetricsQuery>) {
// Migrate old targets // Migrate old targets
const requestTargets = request.targets.map(t => { const requestTargets = request.targets.map((t) => {
// Prevent changes of original object // Prevent changes of original object
const target = _.cloneDeep(t); const target = _.cloneDeep(t);
return migrations.migrate(target); return migrations.migrate(target);
@@ -117,11 +124,16 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
const backendResponsePromise = this.backendQuery({ ...request, targets: requestTargets }); const backendResponsePromise = this.backendQuery({ ...request, targets: requestTargets });
const dbConnectionResponsePromise = this.dbConnectionQuery({ ...request, targets: requestTargets }); const dbConnectionResponsePromise = this.dbConnectionQuery({ ...request, targets: requestTargets });
const frontendResponsePromise = this.frontendQuery({ ...request, targets: requestTargets }); const frontendResponsePromise = this.frontendQuery({ ...request, targets: requestTargets });
const annotationResposePromise = this.annotationRequest({ ...request, targets: requestTargets });
return Promise.all([backendResponsePromise, dbConnectionResponsePromise, frontendResponsePromise]) return Promise.all([
.then(rsp => { backendResponsePromise,
dbConnectionResponsePromise,
frontendResponsePromise,
annotationResposePromise,
]).then((rsp) => {
// Merge backend and frontend queries results // Merge backend and frontend queries results
const [backendRes, dbConnectionRes, frontendRes] = rsp; const [backendRes, dbConnectionRes, frontendRes, annotationRes] = rsp;
if (dbConnectionRes.data) { if (dbConnectionRes.data) {
backendRes.data = backendRes.data.concat(dbConnectionRes.data); backendRes.data = backendRes.data.concat(dbConnectionRes.data);
} }
@@ -129,6 +141,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
backendRes.data = backendRes.data.concat(frontendRes.data); backendRes.data = backendRes.data.concat(frontendRes.data);
} }
if (annotationRes.data) {
backendRes.data = backendRes.data.concat(annotationRes.data);
}
return { return {
data: backendRes.data, data: backendRes.data,
state: LoadingState.Done, state: LoadingState.Done,
@@ -144,21 +160,23 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// Add range variables // Add range variables
request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range)); request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
const queries = _.compact(targets.map((query) => { const queries = _.compact(
// Don't request for hidden targets targets.map((query) => {
if (query.hide) { // Don't request for hidden targets
return null; if (query.hide) {
} return null;
}
this.replaceTargetVariables(query, request); this.replaceTargetVariables(query, request);
return { return {
...query, ...query,
datasourceId: this.datasourceId, datasourceId: this.datasourceId,
intervalMs, intervalMs,
maxDataPoints, maxDataPoints,
}; };
})); })
);
// Return early if no queries exist // Return early if no queries exist
if (!queries.length) { if (!queries.length) {
@@ -175,12 +193,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
let rsp: any; let rsp: any;
try { try {
rsp = await getBackendSrv().fetch({ rsp = await getBackendSrv()
url: '/api/ds/query', .fetch({
method: 'POST', url: '/api/ds/query',
data: body, method: 'POST',
requestId, data: body,
}).toPromise(); requestId,
})
.toPromise();
} catch (err) { } catch (err) {
return toDataQueryResponse(err); return toDataQueryResponse(err);
} }
@@ -198,8 +218,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
} }
async frontendQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> { async frontendQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> {
const frontendTargets = request.targets.filter(t => !(this.isBackendTarget(t) || this.isDBConnectionTarget(t))); const frontendTargets = request.targets.filter((t) => !(this.isBackendTarget(t) || this.isDBConnectionTarget(t)));
const promises = _.map(frontendTargets, target => { const promises = _.map(frontendTargets, (target) => {
// Don't request for hidden targets // Don't request for hidden targets
if (target.hide) { if (target.hide) {
return []; return [];
@@ -233,50 +253,52 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// Data for panel (all targets) // Data for panel (all targets)
return Promise.all(_.flatten(promises)) return Promise.all(_.flatten(promises))
.then(_.flatten) .then(_.flatten)
.then(data => { .then((data) => {
if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) { if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) {
data = responseHandler.alignFrames(data); data = responseHandler.alignFrames(data);
if (responseHandler.isConvertibleToWide(data)) { if (responseHandler.isConvertibleToWide(data)) {
console.log('Converting response to the wide format'); console.log('Converting response to the wide format');
data = responseHandler.convertToWide(data); data = responseHandler.convertToWide(data);
}
} }
} return { data };
return { data }; });
});
} }
async dbConnectionQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> { async dbConnectionQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> {
const targets = request.targets.filter(this.isDBConnectionTarget); const targets = request.targets.filter(this.isDBConnectionTarget);
const queries = _.compact(targets.map((target) => { const queries = _.compact(
// Don't request for hidden targets targets.map((target) => {
if (target.hide) { // Don't request for hidden targets
return []; if (target.hide) {
}
// Add range variables
request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
this.replaceTargetVariables(target, request);
const timeRange = this.buildTimeRange(request, target);
const useTrends = this.isUseTrends(timeRange);
if (!target.queryType || target.queryType === c.MODE_METRICS) {
return this.queryNumericData(target, timeRange, useTrends, request);
} else if (target.queryType === c.MODE_ITEMID) {
// Item ID query
if (!target.itemids) {
return []; return [];
} }
return this.queryItemIdData(target, timeRange, useTrends, request);
} else { // Add range variables
return []; request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
} this.replaceTargetVariables(target, request);
})); const timeRange = this.buildTimeRange(request, target);
const useTrends = this.isUseTrends(timeRange);
if (!target.queryType || target.queryType === c.MODE_METRICS) {
return this.queryNumericData(target, timeRange, useTrends, request);
} else if (target.queryType === c.MODE_ITEMID) {
// Item ID query
if (!target.itemids) {
return [];
}
return this.queryItemIdData(target, timeRange, useTrends, request);
} else {
return [];
}
})
);
const promises: Promise<DataQueryResponse> = Promise.all(queries) const promises: Promise<DataQueryResponse> = Promise.all(queries)
.then(_.flatten) .then(_.flatten)
.then(data => ({ data })); .then((data) => ({ data }));
return promises; return promises;
} }
@@ -300,7 +322,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
*/ */
async queryNumericData(target, timeRange, useTrends, request): Promise<any> { async queryNumericData(target, timeRange, useTrends, request): Promise<any> {
const getItemOptions = { const getItemOptions = {
itemtype: 'num' itemtype: 'num',
}; };
const items = await this.zabbix.getItemsFromTarget(target, getItemOptions); const items = await this.zabbix.getItemsFromTarget(target, getItemOptions);
@@ -343,7 +365,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
url: `/api/datasources/${this.datasourceId}/resources/db-connection-post`, url: `/api/datasources/${this.datasourceId}/resources/db-connection-post`,
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
hideFromInspector: false, hideFromInspector: false,
data: { data: {
@@ -379,10 +401,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
getTrendValueType(target) { getTrendValueType(target) {
// Find trendValue() function and get specified trend value // Find trendValue() function and get specified trend value
const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
const trendValueFunc = _.find(target.functions, func => { const trendValueFunc = _.find(target.functions, (func) => {
return _.includes(trendFunctions, func.def.name); return _.includes(trendFunctions, func.def.name);
}); });
return trendValueFunc ? trendValueFunc.params[0] : "avg"; return trendValueFunc ? trendValueFunc.params[0] : 'avg';
} }
sortByRefId(response: DataQueryResponse) { sortByRefId(response: DataQueryResponse) {
@@ -413,18 +435,19 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
*/ */
queryTextData(target, timeRange) { queryTextData(target, timeRange) {
const options = { const options = {
itemtype: 'text' itemtype: 'text',
}; };
return this.zabbix.getItemsFromTarget(target, options) return this.zabbix
.then(items => { .getItemsFromTarget(target, options)
return this.zabbix.getHistoryText(items, timeRange, target); .then((items) => {
}) return this.zabbix.getHistoryText(items, timeRange, target);
.then(result => { })
if (target.resultFormat !== 'table') { .then((result) => {
return result.map(s => responseHandler.seriesToDataFrame(s, target, [], FieldType.string)); if (target.resultFormat !== 'table') {
} return result.map((s) => responseHandler.seriesToDataFrame(s, target, [], FieldType.string));
return result; }
}); return result;
});
} }
/** /**
@@ -433,14 +456,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
queryItemIdData(target, timeRange, useTrends, options) { queryItemIdData(target, timeRange, useTrends, options) {
let itemids = target.itemids; let itemids = target.itemids;
itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat); itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
itemids = _.map(itemids.split(','), itemid => itemid.trim()); itemids = _.map(itemids.split(','), (itemid) => itemid.trim());
if (!itemids) { if (!itemids) {
return []; return [];
} }
return this.zabbix.getItemsByIDs(itemids) return this.zabbix.getItemsByIDs(itemids).then((items) => {
.then(items => {
return this.queryNumericDataForItems(items, target, timeRange, useTrends, options); return this.queryNumericDataForItems(items, target, timeRange, useTrends, options);
}); });
} }
@@ -468,7 +490,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
let itservices = await this.zabbix.getITServices(itServiceFilter); let itservices = await this.zabbix.getITServices(itServiceFilter);
if (request.isOldVersion) { if (request.isOldVersion) {
itservices = _.filter(itservices, { 'serviceid': target.itservice?.serviceid }); itservices = _.filter(itservices, { serviceid: target.itservice?.serviceid });
} }
const itservicesdp = await this.zabbix.getSLA(itservices, timeRange, target, request); const itservicesdp = await this.zabbix.getSLA(itservices, timeRange, target, request);
const backendRequest = responseHandler.itServiceResponseToTimeSeries(itservicesdp, target.slaInterval); const backendRequest = responseHandler.itServiceResponseToTimeSeries(itservicesdp, target.slaInterval);
@@ -478,8 +500,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
queryTriggersData(target, timeRange) { queryTriggersData(target, timeRange) {
const [timeFrom, timeTo] = timeRange; const [timeFrom, timeTo] = timeRange;
return this.zabbix.getHostsFromTarget(target) return this.zabbix.getHostsFromTarget(target).then((results) => {
.then(results => {
const [hosts, apps] = results; const [hosts, apps] = results;
if (hosts.length) { if (hosts.length) {
const hostids = _.map(hosts, 'hostid'); const hostids = _.map(hosts, 'hostid');
@@ -489,14 +510,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
acknowledged: target.triggers.acknowledged, acknowledged: target.triggers.acknowledged,
count: target.triggers.count, count: target.triggers.count,
timeFrom: timeFrom, timeFrom: timeFrom,
timeTo: timeTo timeTo: timeTo,
}; };
const groupFilter = target.group.filter; const groupFilter = target.group.filter;
return Promise.all([ return Promise.all([
this.zabbix.getHostAlerts(hostids, appids, options), this.zabbix.getHostAlerts(hostids, appids, options),
this.zabbix.getGroups(groupFilter) this.zabbix.getGroups(groupFilter),
]) ]).then(([triggers, groups]) => {
.then(([triggers, groups]) => {
return responseHandler.handleTriggersResponse(triggers, groups, timeRange); return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
}); });
} else { } else {
@@ -536,7 +556,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// replaceTemplateVars() builds regex-like string, so we should trim it. // replaceTemplateVars() builds regex-like string, so we should trim it.
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', ''); const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', '');
const tags = utils.parseTags(tagsFilterStr); const tags = utils.parseTags(tagsFilterStr);
tags.forEach(tag => { tags.forEach((tag) => {
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal // Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
tag.operator = 1; tag.operator = 1;
}); });
@@ -556,9 +576,9 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
} }
if (target.options?.minSeverity) { if (target.options?.minSeverity) {
let severities = [0, 1, 2, 3, 4, 5].filter(v => v >= target.options?.minSeverity); let severities = [0, 1, 2, 3, 4, 5].filter((v) => v >= target.options?.minSeverity);
if (target.options?.severities) { if (target.options?.severities) {
severities = severities.filter(v => target.options?.severities.includes(v)); severities = severities.filter((v) => target.options?.severities.includes(v));
} }
problemsOptions.severities = severities; problemsOptions.severities = severities;
} }
@@ -567,27 +587,30 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
if (showProblems === ShowProblemTypes.History || target.options?.useTimeRange) { if (showProblems === ShowProblemTypes.History || target.options?.useTimeRange) {
problemsOptions.timeFrom = timeFrom; problemsOptions.timeFrom = timeFrom;
problemsOptions.timeTo = timeTo; problemsOptions.timeTo = timeTo;
getProblemsPromise = this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions); getProblemsPromise = this.zabbix.getProblemsHistory(
groupFilter,
hostFilter,
appFilter,
proxyFilter,
problemsOptions
);
} else { } else {
getProblemsPromise = this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions); getProblemsPromise = this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions);
} }
const problemsPromises = Promise.all([ const problemsPromises = Promise.all([getProblemsPromise, getProxiesPromise])
getProblemsPromise, .then(([problems, sourceProxies]) => {
getProxiesPromise proxies = _.keyBy(sourceProxies, 'proxyid');
]) return problems;
.then(([problems, sourceProxies]) => { })
proxies = _.keyBy(sourceProxies, 'proxyid'); .then((problems) => problemsHandler.setMaintenanceStatus(problems))
return problems; .then((problems) => problemsHandler.setAckButtonStatus(problems, showAckButton))
}) .then((problems) => problemsHandler.filterTriggersPre(problems, replacedTarget))
.then(problems => problemsHandler.setMaintenanceStatus(problems)) .then((problems) => problemsHandler.sortProblems(problems, target))
.then(problems => problemsHandler.setAckButtonStatus(problems, showAckButton)) .then((problems) => problemsHandler.addTriggerDataSource(problems, target))
.then(problems => problemsHandler.filterTriggersPre(problems, replacedTarget)) .then((problems) => problemsHandler.addTriggerHostProxy(problems, proxies));
.then(problems => problemsHandler.sortProblems(problems, target))
.then(problems => problemsHandler.addTriggerDataSource(problems, target))
.then(problems => problemsHandler.addTriggerHostProxy(problems, proxies));
return problemsPromises.then(problems => { return problemsPromises.then((problems) => {
const problemsDataFrame = problemsHandler.toDataFrame(problems); const problemsDataFrame = problemsHandler.toDataFrame(problems);
return problemsDataFrame; return problemsDataFrame;
}); });
@@ -604,35 +627,35 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
message += `, DB connector type: ${dbConnectorStatus.dsType}`; message += `, DB connector type: ${dbConnectorStatus.dsType}`;
} }
return { return {
status: "success", status: 'success',
title: "Success", title: 'Success',
message: message message: message,
}; };
} catch (error) { } catch (error) {
if (error instanceof ZabbixAPIError) { if (error instanceof ZabbixAPIError) {
return { return {
status: "error", status: 'error',
title: error.message, title: error.message,
message: error.message message: error.message,
}; };
} else if (error.data && error.data.message) { } else if (error.data && error.data.message) {
return { return {
status: "error", status: 'error',
title: "Zabbix Client Error", title: 'Zabbix Client Error',
message: error.data.message message: error.data.message,
}; };
} else if (typeof (error) === 'string') { } else if (typeof error === 'string') {
return { return {
status: "error", status: 'error',
title: "Unknown Error", title: 'Unknown Error',
message: error message: error,
}; };
} else { } else {
console.log(error); console.log(error);
return { return {
status: "error", status: 'error',
title: "Connection failed", title: 'Connection failed',
message: "Could not connect to given url" message: 'Could not connect to given url',
}; };
} }
} }
@@ -683,7 +706,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
resultPromise = this.zabbix.getItemTags(queryModel.group, queryModel.host, queryModel.itemTag); resultPromise = this.zabbix.getItemTags(queryModel.group, queryModel.host, queryModel.itemTag);
break; break;
case VariableQueryTypes.Item: case VariableQueryTypes.Item:
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, null, queryModel.item); resultPromise = this.zabbix.getItems(
queryModel.group,
queryModel.host,
queryModel.application,
null,
queryModel.item
);
break; break;
case VariableQueryTypes.ItemValues: case VariableQueryTypes.ItemValues:
const range = options?.range; const range = options?.range;
@@ -694,7 +723,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
break; break;
} }
return resultPromise.then(metrics => { return resultPromise.then((metrics) => {
return _.map(metrics, formatMetric); return _.map(metrics, formatMetric);
}); });
} }
@@ -718,74 +747,85 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// Annotations // // Annotations //
///////////////// /////////////////
annotationQuery(options) { async annotationRequest(request: DataQueryRequest<any>): Promise<DataQueryResponse> {
const targets = request.targets.filter((t) => t.fromAnnotations);
if (!targets.length) {
return Promise.resolve({ data: [] });
}
const events = await this.annotationQueryLegacy({ ...request, targets });
return { data: [toDataFrame(events)] };
}
annotationQueryLegacy(options) {
const timeRange = options.range || options.rangeRaw; const timeRange = options.range || options.rangeRaw;
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000); const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000); const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
const annotation = options.annotation; const annotation = options.targets[0];
// Show all triggers // Show all triggers
const problemsOptions: any = { const problemsOptions: any = {
value: annotation.showOkEvents ? ['0', '1'] : '1', value: annotation.options.showOkEvents ? ['0', '1'] : '1',
valueFromEvent: true, valueFromEvent: true,
timeFrom, timeFrom,
timeTo, timeTo,
}; };
if (annotation.minseverity) { if (annotation.options.minSeverity) {
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity)); const severities = [0, 1, 2, 3, 4, 5].filter((v) => v >= Number(annotation.options.minSeverity));
problemsOptions.severities = severities; problemsOptions.severities = severities;
} }
const groupFilter = this.replaceTemplateVars(annotation.group, {}); const groupFilter = this.replaceTemplateVars(annotation.group.filter, {});
const hostFilter = this.replaceTemplateVars(annotation.host, {}); const hostFilter = this.replaceTemplateVars(annotation.host.filter, {});
const appFilter = this.replaceTemplateVars(annotation.application, {}); const appFilter = this.replaceTemplateVars(annotation.application.filter, {});
const proxyFilter = undefined; const proxyFilter = undefined;
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions) return this.zabbix
.then(problems => { .getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
// Filter triggers by description .then((problems) => {
const problemName = this.replaceTemplateVars(annotation.trigger, {}); // Filter triggers by description
if (utils.isRegex(problemName)) { const problemName = this.replaceTemplateVars(annotation.trigger.filter, {});
problems = _.filter(problems, p => { if (utils.isRegex(problemName)) {
return utils.buildRegex(problemName).test(p.description); problems = _.filter(problems, (p) => {
}); return utils.buildRegex(problemName).test(p.description);
} else if (problemName) { });
problems = _.filter(problems, p => { } else if (problemName) {
return p.description === problemName; problems = _.filter(problems, (p) => {
}); return p.description === problemName;
} });
// 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 { // Hide acknowledged events if option enabled
title: p.value === '1' ? 'Problem' : 'OK', if (annotation.hideAcknowledged) {
time: p.timestamp * 1000, problems = _.filter(problems, (p) => {
annotation: annotation, return !p.acknowledges?.length;
text: p.name + formattedAcknowledges, });
tags: annotationTags, }
};
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,
};
});
}); });
});
} }
// Replace template variables // Replace template variables
replaceTargetVariables(target, options) { replaceTargetVariables(target, options) {
const parts = ['group', 'host', 'application', 'itemTag', 'item']; const parts = ['group', 'host', 'application', 'itemTag', 'item'];
_.forEach(parts, p => { _.forEach(parts, (p) => {
if (target[p] && target[p].filter) { if (target[p] && target[p].filter) {
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars); target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
} }
@@ -799,8 +839,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
target.itemids = this.templateSrv.replace(target.itemids, options.scopedVars, zabbixItemIdsTemplateFormat); target.itemids = this.templateSrv.replace(target.itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
} }
_.forEach(target.functions, func => { _.forEach(target.functions, (func) => {
func.params = _.map(func.params, param => { func.params = _.map(func.params, (param) => {
if (typeof param === 'number') { if (typeof param === 'number') {
return +this.templateSrv.replace(param.toString(), options.scopedVars); return +this.templateSrv.replace(param.toString(), options.scopedVars);
} else { } else {
@@ -814,10 +854,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
const [timeFrom, timeTo] = timeRange; const [timeFrom, timeTo] = timeRange;
const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000); const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
const useTrends = this.trends && ( const useTrends = this.trends && (timeFrom < useTrendsFrom || timeTo - timeFrom > useTrendsRange);
(timeFrom < useTrendsFrom) ||
(timeTo - timeFrom > useTrendsRange)
);
return useTrends; return useTrends;
} }
@@ -826,23 +863,21 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return false; return false;
} }
return target.queryType === c.MODE_METRICS || return target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID;
target.queryType === c.MODE_ITEMID;
}; };
isDBConnectionTarget = (target: any): boolean => { isDBConnectionTarget = (target: any): boolean => {
return this.enableDirectDBConnection && return this.enableDirectDBConnection && (target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID);
(target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID);
}; };
} }
function bindFunctionDefs(functionDefs, category) { function bindFunctionDefs(functionDefs, category) {
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
const aggFuncDefs = _.filter(functionDefs, func => { const aggFuncDefs = _.filter(functionDefs, (func) => {
return _.includes(aggregationFunctions, func.def.name); return _.includes(aggregationFunctions, func.def.name);
}); });
return _.map(aggFuncDefs, func => { return _.map(aggFuncDefs, (func) => {
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params); const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(dataProcessor.metricFunctions); return funcInstance.bindFunction(dataProcessor.metricFunctions);
}); });
@@ -850,7 +885,7 @@ function bindFunctionDefs(functionDefs, category) {
function getConsolidateBy(target) { function getConsolidateBy(target) {
let consolidateBy; let consolidateBy;
const funcDef = _.find(target.functions, func => { const funcDef = _.find(target.functions, (func) => {
return func.def.name === 'consolidateBy'; return func.def.name === 'consolidateBy';
}); });
if (funcDef && funcDef.params && funcDef.params.length) { if (funcDef && funcDef.params && funcDef.params.length) {
@@ -862,7 +897,7 @@ function getConsolidateBy(target) {
function formatMetric(metricObj) { function formatMetric(metricObj) {
return { return {
text: metricObj.name, text: metricObj.name,
expandable: false expandable: false,
}; };
} }

View File

@@ -1,257 +0,0 @@
import coreModule from 'grafana/app/core/core_module';
import _ from 'lodash';
import $ from 'jquery';
import { react2AngularDirective } from './react2angular';
import { FunctionEditor } from './components/FunctionEditor';
/** @ngInject */
export function zabbixFunctionEditor($compile, templateSrv) {
const funcSpanTemplate = `
<zbx-function-editor
func="func"
onRemove="ctrl.handleRemoveFunction"
onMoveLeft="ctrl.handleMoveLeft"
onMoveRight="ctrl.handleMoveRight">
</zbx-function-editor>
<span>(</span>
`;
const paramTemplate =
'<input type="text" style="display:none"' + ' class="input-small tight-form-func-param"></input>';
return {
restrict: 'A',
link: function postLink($scope, elem) {
const $funcLink = $(funcSpanTemplate);
const ctrl = $scope.ctrl;
const func = $scope.func;
let scheduledRelink = false;
let paramCountAtLink = 0;
let cancelBlur = null;
ctrl.handleRemoveFunction = func => {
ctrl.removeFunction(func);
};
ctrl.handleMoveLeft = func => {
ctrl.moveFunction(func, -1);
};
ctrl.handleMoveRight = func => {
ctrl.moveFunction(func, 1);
};
function clickFuncParam(this: any, paramIndex) {
/*jshint validthis:true */
const $link = $(this);
const $comma = $link.prev('.comma');
const $input = $link.next();
$input.val(func.params[paramIndex]);
$comma.removeClass('query-part__last');
$link.hide();
$input.show();
$input.focus();
$input.select();
const typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function scheduledRelinkIfNeeded() {
if (paramCountAtLink === func.params.length) {
return;
}
if (!scheduledRelink) {
scheduledRelink = true;
setTimeout(() => {
relink();
scheduledRelink = false;
}, 200);
}
}
function paramDef(index) {
if (index < func.def.params.length) {
return func.def.params[index];
}
if ((_.last(func.def.params) as any).multiple) {
return _.assign({}, _.last(func.def.params), { optional: true });
}
return {};
}
function switchToLink(inputElem, paramIndex) {
/*jshint validthis:true */
const $input = $(inputElem);
clearTimeout(cancelBlur);
cancelBlur = null;
const $link = $input.prev();
const $comma = $link.prev('.comma');
const newValue = $input.val();
// remove optional empty params
if (newValue !== '' || paramDef(paramIndex).optional) {
func.updateParam(newValue, paramIndex);
$link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
}
scheduledRelinkIfNeeded();
$scope.$apply(() => {
ctrl.targetChanged();
});
if ($link.hasClass('query-part__last') && newValue === '') {
$comma.addClass('query-part__last');
} else {
$link.removeClass('query-part__last');
}
$input.hide();
$link.show();
}
// this = input element
function inputBlur(this: any, paramIndex) {
/*jshint validthis:true */
const inputElem = this;
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout(() => {
switchToLink(inputElem, paramIndex);
}, 200);
}
function inputKeyPress(this: any, paramIndex, e) {
/*jshint validthis:true */
if (e.which === 13) {
$(this).blur();
}
}
function inputKeyDown(this: any) {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
let options = paramDef(paramIndex).options;
if (paramDef(paramIndex).type === 'int' || paramDef(paramIndex).type === 'float') {
options = _.map(options, val => {
return val.toString();
});
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: value => {
$input.val(value);
switchToLink($input[0], paramIndex);
return value;
},
});
const typeahead = $input.data('typeahead');
typeahead.lookup = function() {
this.query = this.$element.val() || '';
return this.process(this.source);
};
}
function addElementsAndCompile() {
$funcLink.appendTo(elem);
const defParams: any = _.clone(func.def.params);
const lastParam: any = _.last(func.def.params);
while (func.params.length >= defParams.length && lastParam && lastParam.multiple) {
defParams.push(_.assign({}, lastParam, { optional: true }));
}
_.each(defParams, (param: any, index: number) => {
if (param.optional && func.params.length < index) {
return false;
}
let paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
const hasValue = paramValue !== null && paramValue !== undefined;
const last = index >= func.params.length - 1 && param.optional && !hasValue;
if (last && param.multiple) {
paramValue = '+';
}
if (index > 0) {
$('<span class="comma' + (last ? ' query-part__last' : '') + '">,&nbsp; </span>').appendTo(elem);
}
const $paramLink = $(
'<a ng-click="" class="graphite-func-param-link' +
(last ? ' query-part__last' : '') +
'">' +
(hasValue ? paramValue : '&nbsp;') +
'</a>'
);
const $input = $(paramTemplate);
$input.attr('placeholder', param.name);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (param.options) {
addTypeahead($input, index);
}
return true;
});
$('<span>)</span>').appendTo(elem);
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFirstParam() {
if ($scope.func.added) {
$scope.func.added = false;
setTimeout(() => {
elem
.find('.graphite-func-param-link')
.first()
.click();
}, 10);
}
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFirstParam();
}
relink();
},
};
}
coreModule.directive('zabbixFunctionEditor', zabbixFunctionEditor);
react2AngularDirective('zbxFunctionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);

View File

@@ -1,18 +1,19 @@
import _ from 'lodash'; import _ from 'lodash';
import { FuncDef } from './types';
import { isNumeric } from './utils'; import { isNumeric } from './utils';
const index = []; const index = {};
const categories = { const categories: { [key: string]: FuncDef[] } = {
Transform: [], Transform: [],
Aggregate: [], Aggregate: [],
Filter: [], Filter: [],
Trends: [], Trends: [],
Time: [], Time: [],
Alias: [], Alias: [],
Special: [] Special: [],
}; };
function addFuncDef(funcDef) { function addFuncDef(funcDef: FuncDef) {
funcDef.params = funcDef.params || []; funcDef.params = funcDef.params || [];
funcDef.defaultParams = funcDef.defaultParams || []; funcDef.defaultParams = funcDef.defaultParams || [];
@@ -29,8 +30,8 @@ addFuncDef({
name: 'groupBy', name: 'groupBy',
category: 'Transform', category: 'Transform',
params: [ params: [
{ name: 'interval', type: 'string'}, { name: 'interval', type: 'string' },
{ name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] } { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] },
], ],
defaultParams: ['1m', 'avg'], defaultParams: ['1m', 'avg'],
}); });
@@ -38,18 +39,14 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'scale', name: 'scale',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'factor', type: 'float', options: [100, 0.01, 10, -1] }],
{ name: 'factor', type: 'float', options: [100, 0.01, 10, -1]}
],
defaultParams: [100], defaultParams: [100],
}); });
addFuncDef({ addFuncDef({
name: 'offset', name: 'offset',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'delta', type: 'float', options: [-100, 100] }],
{ name: 'delta', type: 'float', options: [-100, 100]}
],
defaultParams: [100], defaultParams: [100],
}); });
@@ -70,18 +67,14 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'movingAverage', name: 'movingAverage',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'factor', type: 'int', options: [6, 10, 60, 100, 600] }],
{ name: 'factor', type: 'int', options: [6, 10, 60, 100, 600] }
],
defaultParams: [10], defaultParams: [10],
}); });
addFuncDef({ addFuncDef({
name: 'exponentialMovingAverage', name: 'exponentialMovingAverage',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'smoothing', type: 'float', options: [6, 10, 60, 100, 600] }],
{ name: 'smoothing', type: 'float', options: [6, 10, 60, 100, 600] }
],
defaultParams: [0.2], defaultParams: [0.2],
}); });
@@ -90,7 +83,7 @@ addFuncDef({
category: 'Transform', category: 'Transform',
params: [ params: [
{ name: 'interval', type: 'string' }, { name: 'interval', type: 'string' },
{ name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] } { name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] },
], ],
defaultParams: ['1m', 95], defaultParams: ['1m', 95],
}); });
@@ -98,27 +91,21 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'removeAboveValue', name: 'removeAboveValue',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'number', type: 'float' }],
{name: 'number', type: 'float'},
],
defaultParams: [0], defaultParams: [0],
}); });
addFuncDef({ addFuncDef({
name: 'removeBelowValue', name: 'removeBelowValue',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'number', type: 'float' }],
{name: 'number', type: 'float'},
],
defaultParams: [0], defaultParams: [0],
}); });
addFuncDef({ addFuncDef({
name: 'transformNull', name: 'transformNull',
category: 'Transform', category: 'Transform',
params: [ params: [{ name: 'number', type: 'float' }],
{name: 'number', type: 'float'}
],
defaultParams: [0], defaultParams: [0],
}); });
@@ -129,7 +116,7 @@ addFuncDef({
category: 'Aggregate', category: 'Aggregate',
params: [ params: [
{ name: 'interval', type: 'string' }, { name: 'interval', type: 'string' },
{ name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] } { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] },
], ],
defaultParams: ['1m', 'avg'], defaultParams: ['1m', 'avg'],
}); });
@@ -146,7 +133,7 @@ addFuncDef({
category: 'Aggregate', category: 'Aggregate',
params: [ params: [
{ name: 'interval', type: 'string' }, { name: 'interval', type: 'string' },
{ name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] } { name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] },
], ],
defaultParams: ['1m', 95], defaultParams: ['1m', 95],
}); });
@@ -158,7 +145,7 @@ addFuncDef({
category: 'Filter', category: 'Filter',
params: [ params: [
{ name: 'number', type: 'int' }, { name: 'number', type: 'int' },
{ name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] } { name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] },
], ],
defaultParams: [5, 'avg'], defaultParams: [5, 'avg'],
}); });
@@ -168,7 +155,7 @@ addFuncDef({
category: 'Filter', category: 'Filter',
params: [ params: [
{ name: 'number', type: 'int' }, { name: 'number', type: 'int' },
{ name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] } { name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] },
], ],
defaultParams: [5, 'avg'], defaultParams: [5, 'avg'],
}); });
@@ -176,10 +163,8 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'sortSeries', name: 'sortSeries',
category: 'Filter', category: 'Filter',
params: [ params: [{ name: 'direction', type: 'string', options: ['asc', 'desc'] }],
{ name: 'direction', type: 'string', options: ['asc', 'desc'] } defaultParams: ['asc'],
],
defaultParams: ['asc']
}); });
// Trends // Trends
@@ -187,9 +172,7 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'trendValue', name: 'trendValue',
category: 'Trends', category: 'Trends',
params: [ params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }
],
defaultParams: ['avg'], defaultParams: ['avg'],
}); });
@@ -198,9 +181,7 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'timeShift', name: 'timeShift',
category: 'Time', category: 'Time',
params: [ params: [{ name: 'interval', type: 'string', options: ['24h', '7d', '1M', '+24h', '-24h'] }],
{ name: 'interval', type: 'string', options: ['24h', '7d', '1M', '+24h', '-24h']}
],
defaultParams: ['24h'], defaultParams: ['24h'],
}); });
@@ -209,19 +190,15 @@ addFuncDef({
addFuncDef({ addFuncDef({
name: 'setAlias', name: 'setAlias',
category: 'Alias', category: 'Alias',
params: [ params: [{ name: 'alias', type: 'string' }],
{ name: 'alias', type: 'string' } defaultParams: [],
],
defaultParams: []
}); });
addFuncDef({ addFuncDef({
name: 'setAliasByRegex', name: 'setAliasByRegex',
category: 'Alias', category: 'Alias',
params: [ params: [{ name: 'aliasByRegex', type: 'string' }],
{ name: 'aliasByRegex', type: 'string' } defaultParams: [],
],
defaultParams: []
}); });
addFuncDef({ addFuncDef({
@@ -229,18 +206,16 @@ addFuncDef({
category: 'Alias', category: 'Alias',
params: [ params: [
{ name: 'regexp', type: 'string' }, { name: 'regexp', type: 'string' },
{ name: 'newAlias', type: 'string' } { name: 'newAlias', type: 'string' },
], ],
defaultParams: ['/(.*)/', '$1'] defaultParams: ['/(.*)/', '$1'],
}); });
// Special // Special
addFuncDef({ addFuncDef({
name: 'consolidateBy', name: 'consolidateBy',
category: 'Special', category: 'Special',
params: [ params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }
],
defaultParams: ['avg'], defaultParams: ['avg'],
}); });
@@ -271,7 +246,6 @@ class FuncInstance {
bindFunction(metricFunctions) { bindFunction(metricFunctions) {
const func = metricFunctions[this.def.name]; const func = metricFunctions[this.def.name];
if (func) { if (func) {
// Bind function arguments // Bind function arguments
let bindedFunc = func; let bindedFunc = func;
let param; let param;
@@ -279,8 +253,7 @@ class FuncInstance {
param = this.params[i]; param = this.params[i];
// Convert numeric params // Convert numeric params
if (this.def.params[i].type === 'int' || if (this.def.params[i].type === 'int' || this.def.params[i].type === 'float') {
this.def.params[i].type === 'float') {
param = Number(param); param = Number(param);
} }
bindedFunc = _.partial(bindedFunc, param); bindedFunc = _.partial(bindedFunc, param);
@@ -295,17 +268,13 @@ class FuncInstance {
const str = this.def.name + '('; const str = this.def.name + '(';
const parameters = _.map(this.params, (value, index) => { const parameters = _.map(this.params, (value, index) => {
const paramType = this.def.params[index].type; const paramType = this.def.params[index].type;
if (paramType === 'int' || if (paramType === 'int' || paramType === 'float' || paramType === 'value_or_series' || paramType === 'boolean') {
paramType === 'float' ||
paramType === 'value_or_series' ||
paramType === 'boolean') {
return value; return value;
} else if (paramType === 'int_or_interval' && isNumeric(value)) { } else if (paramType === 'int_or_interval' && isNumeric(value)) {
return value; return value;
} }
return "'" + value + "'"; return "'" + value + "'";
}); });
if (metricExp) { if (metricExp) {
@@ -335,7 +304,7 @@ class FuncInstance {
if (strValue === '' && this.def.params[index].optional) { if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1); this.params.splice(index, 1);
}else { } else {
this.params[index] = strValue; this.params[index] = strValue;
} }

View File

@@ -9,9 +9,11 @@ import * as c from './constants';
export function isGrafana2target(target) { export function isGrafana2target(target) {
if (!target.mode || target.mode === 0 || target.mode === 2) { if (!target.mode || target.mode === 0 || target.mode === 2) {
if ((target.hostFilter || target.itemFilter || target.downsampleFunction || if (
(target.host && target.host.host)) && (target.hostFilter || target.itemFilter || target.downsampleFunction || (target.host && target.host.host)) &&
(target.item.filter === undefined && target.host.filter === undefined)) { target.item.filter === undefined &&
target.host.filter === undefined
) {
return true; return true;
} else { } else {
return false; return false;
@@ -22,10 +24,10 @@ export function isGrafana2target(target) {
} }
export function migrateFrom2To3version(target: ZabbixMetricsQuery) { export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
target.group.filter = target.group.name === "*" ? "/.*/" : target.group.name; target.group.filter = target.group.name === '*' ? '/.*/' : target.group.name;
target.host.filter = target.host.name === "*" ? convertToRegex(target.hostFilter) : target.host.name; target.host.filter = target.host.name === '*' ? convertToRegex(target.hostFilter) : target.host.name;
target.application.filter = target.application.name === "*" ? "" : target.application.name; target.application.filter = target.application.name === '*' ? '' : target.application.name;
target.item.filter = target.item.name === "All" ? convertToRegex(target.itemFilter) : target.item.name; target.item.filter = target.item.name === 'All' ? convertToRegex(target.itemFilter) : target.item.name;
return target; return target;
} }
@@ -77,6 +79,12 @@ function migrateApplications(target) {
} }
} }
function migrateSLAProperty(target) {
if (target.slaProperty?.property) {
target.slaProperty = target.slaProperty?.property;
}
}
export function migrate(target) { export function migrate(target) {
target.resultFormat = target.resultFormat || 'time_series'; target.resultFormat = target.resultFormat || 'time_series';
target = fixTargetGroup(target); target = fixTargetGroup(target);
@@ -88,12 +96,13 @@ export function migrate(target) {
migrateSLA(target); migrateSLA(target);
migrateProblemSort(target); migrateProblemSort(target);
migrateApplications(target); migrateApplications(target);
migrateSLAProperty(target);
return target; return target;
} }
function fixTargetGroup(target) { function fixTargetGroup(target) {
if (target.group && Array.isArray(target.group)) { if (target.group && Array.isArray(target.group)) {
target.group = { 'filter': "" }; target.group = { filter: '' };
} }
return target; return target;
} }
@@ -128,7 +137,7 @@ export function migrateDSConfig(jsonData) {
} }
if (oldVersion < 3) { if (oldVersion < 3) {
jsonData.timeout = (jsonData.timeout as string) === "" ? null : Number(jsonData.timeout as string); jsonData.timeout = (jsonData.timeout as string) === '' ? null : Number(jsonData.timeout as string);
} }
return jsonData; return jsonData;
@@ -143,3 +152,34 @@ function shouldMigrateDSConfig(jsonData): boolean {
} }
return false; return false;
} }
const getDefaultAnnotationTarget = (json: any) => {
return {
group: { filter: json.group ?? '' },
host: { filter: json.host ?? '' },
application: { filter: json.application ?? '' },
trigger: { filter: json.trigger ?? '' },
options: {
minSeverity: json.minseverity ?? 0,
showOkEvents: json.showOkEvents ?? false,
hideAcknowledged: json.hideAcknowledged ?? false,
showHostname: json.showHostname ?? false,
},
};
};
export const prepareAnnotation = (json: any) => {
const defaultTarget = getDefaultAnnotationTarget(json);
json.target = {
...defaultTarget,
...json.target,
fromAnnotations: true,
options: {
...defaultTarget.options!,
...json.target?.options,
},
};
return json;
};

View File

@@ -1,25 +1,18 @@
import { DataSourcePlugin } from '@grafana/data'; import { DataSourcePlugin } from '@grafana/data';
import { loadPluginCss } from '@grafana/runtime'; import { loadPluginCss } from '@grafana/runtime';
import { ZabbixDatasource } from './datasource'; import { ZabbixDatasource } from './datasource';
import { ZabbixQueryController } from './query.controller'; import { QueryEditor } from './components/QueryEditor';
import { ZabbixVariableQueryEditor } from './components/VariableQueryEditor'; import { ZabbixVariableQueryEditor } from './components/VariableQueryEditor';
import { ConfigEditor } from './components/ConfigEditor'; import { ConfigEditor } from './components/ConfigEditor';
import './add-metric-function.directive'; import '../sass/grafana-zabbix.dark.scss';
import './metric-function-editor.directive'; import '../sass/grafana-zabbix.light.scss';
class ZabbixAnnotationsQueryController {
static templateUrl = 'datasource-zabbix/partials/annotations.editor.html';
}
ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html';
loadPluginCss({ loadPluginCss({
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css' light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css',
}); });
export const plugin = new DataSourcePlugin(ZabbixDatasource) export const plugin = new DataSourcePlugin(ZabbixDatasource)
.setConfigEditor(ConfigEditor) .setConfigEditor(ConfigEditor)
.setQueryCtrl(ZabbixQueryController) .setQueryEditor(QueryEditor)
.setAnnotationQueryCtrl(ZabbixAnnotationsQueryController)
.setVariableQueryEditor(ZabbixVariableQueryEditor); .setVariableQueryEditor(ZabbixVariableQueryEditor);

View File

@@ -1,69 +0,0 @@
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Group</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.group">
</input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Host</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.host">
</input>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Application</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.application">
</input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Trigger</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.trigger">
</input>
</div>
</div>
</div>
<div class="gf-form-group">
<h6>Options</h6>
<div class="gf-form">
<span class="gf-form-label width-12">Minimum severity</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto"
ng-init='ctrl.annotation.minseverity = ctrl.annotation.minseverity || 0'
ng-model='ctrl.annotation.minseverity'
ng-options="v as k for (k, v) in {
'Not classified': 0,
'Information': 1,
'Warning': 2,
'Average': 3,
'High': 4,
'Disaster': 5
}"
ng-change="render()">
</select>
</div>
</div>
<gf-form-switch class="gf-form" label-class="width-12"
label="Show OK events"
checked="ctrl.annotation.showOkEvents">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Hide acknowledged events"
checked="ctrl.annotation.hideAcknowledged">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Show hostname"
checked="ctrl.annotation.showHostname">
</gf-form-switch>
</div>

View File

@@ -1,396 +0,0 @@
<query-editor-row query-ctrl="ctrl" can-collapse="false">
<div class="gf-form-inline">
<div class="gf-form max-width-20">
<label class="gf-form-label width-7">Query Mode</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-change="ctrl.switchEditorMode(ctrl.target.queryType)"
ng-model="ctrl.target.queryType"
ng-options="m.queryType as m.text for m in ctrl.editorModes">
</select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Format As</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat"
ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- IT Service editor -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.ITSERVICE">
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">IT Service</label>
<input type="text"
ng-model="ctrl.target.itServiceFilter"
bs-typeahead="ctrl.getITServices"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.itServiceFilter),
'zbx-regex': ctrl.isRegex(ctrl.target.itServiceFilter)
}">
</input>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Property</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.slaProperty"
ng-options="slaProperty.name for slaProperty in ctrl.slaPropertyList track by slaProperty.name">
</select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Interval</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.slaInterval"
ng-options="i.value as i.text for i in ctrl.slaIntervals">
</select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<!-- Select Group -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Group</label>
<input type="text"
ng-model="ctrl.target.group.filter"
bs-typeahead="ctrl.getGroupNames"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.group.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.group.filter)
}"></input>
</div>
<!-- Select Host -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Host</label>
<input type="text"
ng-model="ctrl.target.host.filter"
bs-typeahead="ctrl.getHostNames"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.host.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.host.filter)
}">
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Proxy</label>
<input type="text"
ng-model="ctrl.target.proxy.filter"
bs-typeahead="ctrl.getProxyNames"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input width-14"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.proxy.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.proxy.filter)
}">
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<!-- Select Application -->
<div class="gf-form max-width-20" ng-show="!ctrl.appFilterDisabled()">
<label class="gf-form-label query-keyword width-7">Application</label>
<input type="text"
ng-model="ctrl.target.application.filter"
bs-typeahead="ctrl.getApplicationNames"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.application.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.application.filter)
}">
</div>
<!-- Select item tags -->
<div class="gf-form max-width-20" ng-show="ctrl.appFilterDisabled()">
<label class="gf-form-label query-keyword width-7">Item tag</label>
<input type="text"
ng-model="ctrl.target.itemTag.filter"
bs-typeahead="ctrl.getItemTags"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.itemTag.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.itemTag.filter)
}">
</div>
<!-- Select Item -->
<div class="gf-form max-width-20"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Item</label>
<input type="text"
ng-model="ctrl.target.item.filter"
bs-typeahead="ctrl.getItemNames"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.item.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.item.filter)
}">
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Problem</label>
<input type="text"
ng-model="ctrl.target.trigger.filter"
ng-blur="ctrl.onTargetBlur()"
placeholder="Problem name"
class="gf-form-input"
ng-style="ctrl.target.trigger.style"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.trigger.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.trigger.filter)
}">
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Tags</label>
<input type="text" class="gf-form-input width-14"
ng-model="ctrl.target.tags.filter"
ng-blur="ctrl.onTargetBlur()"
placeholder="tag1:value1, tag2:value2">
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Show</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-model="ctrl.target.showProblems"
ng-options="v.value as v.text for v in ctrl.showProblemsOptions"
ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Min severity</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-model="ctrl.target.options.minSeverity"
ng-options="v.val as v.text for v in ctrl.severityOptions"
ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-7">Min Severity</label>
<div class="gf-form-select-wrapper width-14">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.minSeverity"
ng-options="s.val as s.text for s in ctrl.severityOptions">
</select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
<!-- Text metric regex -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Text filter</label>
<input type="text"
class="gf-form-input"
ng-model="ctrl.target.textFilter"
spellcheck='false'
placeholder="Text filter (regex)"
ng-blur="ctrl.onTargetBlur()">
</div>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups"
on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Item IDs editor mode -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.ITEMID">
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Item IDs</label>
<input type="text"
ng-model="ctrl.target.itemids"
bs-typeahead="ctrl.getVariables"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.itServiceFilter),
'zbx-regex': ctrl.isRegex(ctrl.target.itServiceFilter)
}">
</input>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Metric processing functions -->
<div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.ITEMID || ctrl.target.queryType == editorMode.ITSERVICE">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Functions</label>
</div>
<div ng-repeat="func in ctrl.target.functions" class="gf-form">
<span zabbix-function-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
</div>
<div class="gf-form dropdown" add-metric-function>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form gf-form--grow">
<label class="gf-form-label gf-form-label--grow">
<a ng-click="ctrl.toggleQueryOptions()">
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
{{ctrl.queryOptionsText}}
</a>
</label>
</div>
<!-- Query options -->
<div class="gf-form-group offset-width-7" ng-if="ctrl.showQueryOptions">
<div ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<gf-form-switch class="gf-form" label-class="width-12"
label="Show disabled items"
checked="ctrl.target.options.showDisabledItems"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Use Zabbix value mapping"
checked="ctrl.target.options.useZabbixValueMapping"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Disable data alignment"
checked="ctrl.target.options.disableDataAlignment"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
</div>
<div class="gf-form-group offset-width-7"
ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
<div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-12"
label="Skip empty values"
checked="ctrl.target.options.skipEmptyValues"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
</div>
</div>
<div class="gf-form-group"
ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
label-class="width-9"
label="Count"
checked="ctrl.target.triggers.count"
on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form"
ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label width-9">Acknowledged</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-change="ctrl.onQueryOptionChange()"
ng-model="ctrl.target.options.acknowledged"
ng-options="a.value as a.text for a in ctrl.ackFilters">
</select>
</div>
</div>
<div ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form">
<label class="gf-form-label width-9">Sort by</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-model="ctrl.target.options.sortProblems"
ng-options="f.value as f.text for f in ctrl.sortByFields"
ng-change="ctrl.onQueryOptionChange()">
</select>
</div>
</div>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Use time range"
checked="ctrl.target.options.useTimeRange"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Hosts in maintenance"
checked="ctrl.target.options.hostsInMaintenance"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Host proxy"
checked="ctrl.target.options.hostProxy"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-9">Limit triggers</label>
<input class="gf-form-input width-5"
type="number" placeholder="100"
ng-model="ctrl.target.options.limit"
ng-model-onblur ng-change="ctrl.onQueryOptionChange()">
</div>
</div>
</div>
</div>
</query-editor-row>

View File

@@ -1,572 +0,0 @@
import { QueryCtrl } from 'grafana/app/plugins/sdk';
import _ from 'lodash';
import * as c from './constants';
import * as utils from './utils';
import { itemTagToString } from './utils';
import * as metricFunctions from './metricFunctions';
import * as migrations from './migrations';
import { ShowProblemTypes, ZBXItem, ZBXItemTag } from './types';
import { CURRENT_SCHEMA_VERSION } from '../panel-triggers/migrations';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
function getTargetDefaults() {
return {
queryType: c.MODE_METRICS,
group: { 'filter': "" },
host: { 'filter': "" },
application: { 'filter': "" },
itemTag: { 'filter': "" },
item: { 'filter': "" },
functions: [],
triggers: {
'count': true,
'minSeverity': 3,
'acknowledged': 2
},
trigger: { filter: "" },
tags: { filter: "" },
proxy: { filter: "" },
options: {
showDisabledItems: false,
skipEmptyValues: false,
disableDataAlignment: false,
useZabbixValueMapping: 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;
}
function mapSeverityOptionsFromPanel(severityOptions: any[]) {
if (!severityOptions) {
return [0, 1, 2, 3, 4, 5];
}
const severities = [];
for (const sevOption of severityOptions) {
if (sevOption.show) {
severities.push(sevOption.priority);
}
}
return severities;
}
export class ZabbixQueryController extends QueryCtrl {
static templateUrl: string;
zabbix: any;
replaceTemplateVars: any;
templateSrv: TemplateSrv;
editorModes: Array<{ value: string; text: string; queryType: string; }>;
slaPropertyList: Array<{ name: string; property: string; }>;
slaIntervals: Array<{ text: string; value: string; }>;
ackFilters: Array<{ text: string; value: number; }>;
problemAckFilters: string[];
sortByFields: Array<{ text: string; value: string; }>;
showEventsFields: Array<{ text: string; value: number[]; } | { text: string; value: number; }>;
showProblemsOptions: Array<{ text: string; value: string; }>;
resultFormats: Array<{ text: string; value: string; }>;
severityOptions: Array<{ val: number; text: string; }>;
getGroupNames: (...args: any[]) => any;
getHostNames: (...args: any[]) => any;
getApplicationNames: (...args: any[]) => any;
getItemNames: (...args: any[]) => any;
getITServices: (...args: any[]) => any;
getProxyNames: (...args: any[]) => any;
getVariables: (...args: any[]) => any;
init: () => void;
queryOptionsText: string;
metric: any;
showQueryOptions: boolean;
oldTarget: any;
/** @ngInject */
constructor($scope, $injector, $rootScope) {
super($scope, $injector);
this.zabbix = this.datasource.zabbix;
// Use custom format for template variables
this.replaceTemplateVars = this.datasource.replaceTemplateVars;
this.templateSrv = getTemplateSrv();
this.editorModes = [
{ 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 = {
METRICS: c.MODE_METRICS,
TEXT: c.MODE_TEXT,
ITSERVICE: c.MODE_ITSERVICE,
ITEMID: c.MODE_ITEMID,
TRIGGERS: c.MODE_TRIGGERS,
PROBLEMS: c.MODE_PROBLEMS,
};
this.slaPropertyList = [
{ name: "Status", property: "status" },
{ name: "SLA", property: "sla" },
{ name: "OK time", property: "okTime" },
{ name: "Problem time", property: "problemTime" },
{ 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: 'severity' },
];
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.severityOptions = getSeverityOptions();
// Map functions for bs-typeahead
this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList');
this.getHostNames = _.bind(this.getMetricNames, this, 'hostList', true);
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
$rootScope.$on('template-variable-value-updated', () => this.onVariableChange());
// Update metrics when item selected from dropdown
$scope.$on('typeahead-updated', () => {
this.onTargetBlur();
});
this.init = () => {
let target = this.target;
// Migrate old targets
target = migrations.migrate(target);
this.refresh();
const scopeDefaults = {
metric: {},
oldTarget: _.cloneDeep(this.target),
queryOptionsText: this.renderQueryOptionsText()
};
_.defaults(this, scopeDefaults);
// Load default values
const targetDefaults = getTargetDefaults();
_.defaultsDeep(target, targetDefaults);
this.initDefaultQueryMode(target);
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
target.queryType = c.MODE_PROBLEMS;
target.options.severities = mapSeverityOptionsFromPanel(this.panel.triggerSeverity);
}
// Create function instances from saved JSON
target.functions = _.map(target.functions, func => {
return metricFunctions.createFuncInstance(func.def, func.params);
});
if (target.queryType === c.MODE_ITSERVICE) {
_.defaultsDeep(target, getSLATargetDefaults());
}
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();
}
};
// Update panel schema version to prevent unnecessary migrations
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
this.panel.schemaVersion = CURRENT_SCHEMA_VERSION;
}
this.init();
this.queryOptionsText = this.renderQueryOptionsText();
}
initFilters() {
const mode = _.find(this.editorModes, { 'queryType': this.target.queryType });
const itemtype = mode ? mode.value : null;
const promises = [
this.suggestGroups(),
this.suggestHosts(),
this.suggestApps(),
];
if (this.target.queryType === c.MODE_METRICS || this.target.queryType === c.MODE_TEXT) {
promises.push(this.suggestItems(itemtype));
}
if (this.target.queryType === c.MODE_PROBLEMS) {
promises.push(this.suggestProxies());
}
return Promise.all(promises).then(() => {
if (this.zabbix.isZabbix54OrHigher()) {
this.suggestItemTags()
.then(() => this.$scope.$apply());
}
});
}
initDefaultQueryMode(target) {
if (!(target.queryType === c.MODE_METRICS ||
target.queryType === c.MODE_TEXT ||
target.queryType === c.MODE_ITSERVICE ||
target.queryType === c.MODE_ITEMID ||
target.queryType === c.MODE_TRIGGERS ||
target.queryType === c.MODE_PROBLEMS)) {
target.queryType = c.MODE_METRICS;
}
}
// Get list of metric names for bs-typeahead directive
getMetricNames(metricList, addAllValue) {
const metrics = _.uniq(_.map(this.metric[metricList], 'name'));
// Add template variables
_.forEach(this.templateSrv.getVariables(), variable => {
metrics.unshift('$' + variable.name);
});
if (addAllValue) {
metrics.unshift('/.*/');
}
return metrics;
}
getItemTags = () => {
if (!this.metric?.tagList) {
return [];
}
const tags = this.metric.tagList.map(t => itemTagToString(t));
// Add template variables
_.forEach(this.templateSrv.getVariables(), variable => {
tags.unshift('$' + variable.name);
});
return tags;
};
getTemplateVariables() {
return _.map(this.templateSrv.getVariables(), variable => {
return '$' + variable.name;
});
}
isZabbix54OrHigher() {
return this.zabbix.isZabbix54OrHigher();
}
suggestGroups() {
return this.zabbix.getAllGroups()
.then(groups => {
this.metric.groupList = groups;
return groups;
});
}
suggestHosts() {
const groupFilter = this.replaceTemplateVars(this.target.group.filter);
return this.zabbix.getAllHosts(groupFilter)
.then(hosts => {
this.metric.hostList = hosts;
return hosts;
});
}
suggestApps() {
const groupFilter = this.replaceTemplateVars(this.target.group.filter);
const hostFilter = this.replaceTemplateVars(this.target.host.filter);
return this.zabbix.getAllApps(groupFilter, hostFilter)
.then(apps => {
this.metric.appList = apps;
return apps;
});
}
suggestItems(itemtype = 'num') {
const groupFilter = this.replaceTemplateVars(this.target.group.filter);
const hostFilter = this.replaceTemplateVars(this.target.host.filter);
const appFilter = this.replaceTemplateVars(this.target.application.filter);
const itemTagFilter = this.replaceTemplateVars(this.target.itemTag.filter);
const options = {
itemtype: itemtype,
showDisabledItems: this.target.options.showDisabledItems
};
return this.zabbix
.getAllItems(groupFilter, hostFilter, appFilter, itemTagFilter, options)
.then(items => {
this.metric.itemList = items;
return items;
});
}
async suggestItemTags() {
const groupFilter = this.replaceTemplateVars(this.target.group.filter);
const hostFilter = this.replaceTemplateVars(this.target.host.filter);
const items = await this.zabbix.getAllItems(groupFilter, hostFilter, null, null, {});
const tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => {
if (item.tags) {
return item.tags;
} else {
return [];
}
}));
this.metric.tagList = _.uniqBy(tags, t => t.tag + t.value || '');
}
suggestITServices() {
return this.zabbix.getITService()
.then(itservices => {
this.metric.itServiceList = itservices;
return itservices;
});
}
suggestProxies() {
return this.zabbix.getProxies()
.then(response => {
const proxies = _.map(response, 'host');
this.metric.proxyList = proxies;
return proxies;
});
}
isRegex(str) {
return utils.isRegex(str);
}
isVariable(str) {
return utils.isTemplateVariable(str, this.templateSrv.getVariables());
}
onTargetBlur() {
const newTarget = _.cloneDeep(this.target);
if (!_.isEqual(this.oldTarget, this.target)) {
this.oldTarget = newTarget;
this.targetChanged();
}
}
onVariableChange() {
if (this.isContainsVariables()) {
this.targetChanged();
}
}
/**
* Check query for template variables
*/
isContainsVariables() {
return _.some(['group', 'host', 'application'], field => {
if (this.target[field] && this.target[field].filter) {
return utils.isTemplateVariable(this.target[field].filter, this.templateSrv.getVariables());
} else {
return false;
}
});
}
parseTarget() {
// Parse target
}
// Validate target and set validation info
validateTarget() {
// validate
}
targetChanged() {
this.initFilters();
this.parseTarget();
this.refresh();
}
addFunction(funcDef) {
const newFunc = metricFunctions.createFuncInstance(funcDef);
newFunc.added = true;
this.target.functions.push(newFunc);
this.moveAliasFuncLast();
if (newFunc.params.length && newFunc.added ||
newFunc.def.params.length === 0) {
this.targetChanged();
}
}
removeFunction(func) {
this.target.functions = _.without(this.target.functions, func);
this.targetChanged();
}
moveFunction(func, offset) {
const index = this.target.functions.indexOf(func);
// @ts-ignore
_.move(this.target.functions, index, index + offset);
this.targetChanged();
}
moveAliasFuncLast() {
const aliasFunc = _.find(this.target.functions, func => {
return func.def.category === 'Alias';
});
if (aliasFunc) {
this.target.functions = _.without(this.target.functions, aliasFunc);
this.target.functions.push(aliasFunc);
}
}
toggleQueryOptions() {
this.showQueryOptions = !this.showQueryOptions;
}
onQueryOptionChange() {
this.queryOptionsText = this.renderQueryOptionsText();
this.onTargetBlur();
}
renderQueryOptionsText() {
const metricOptionsMap = {
showDisabledItems: "Show disabled items",
disableDataAlignment: "Disable data alignment",
useZabbixValueMapping: "Use Zabbix value mapping",
};
const problemsOptionsMap = {
sortProblems: "Sort problems",
acknowledged: "Acknowledged",
skipEmptyValues: "Skip empty values",
hostsInMaintenance: "Show hosts in maintenance",
limit: "Limit problems",
hostProxy: "Show proxy",
useTimeRange: "Use time range",
};
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 && 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
let optionValue = value;
if (value && value.text) {
optionValue = value.text;
} else if (value && value.value) {
optionValue = value.value;
}
options.push(optionsMap[key] + " = " + optionValue);
}
}
});
return "Options: " + options.join(', ');
}
/**
* Switch query editor to specified mode.
* Modes:
* 0 - items
* 1 - IT services
* 2 - Text metrics
*/
switchEditorMode(mode) {
this.target.queryType = mode;
this.queryOptionsText = this.renderQueryOptionsText();
this.init();
this.targetChanged();
}
appFilterDisabled() {
return !this.zabbix.supportsApplications();
}
}

View File

@@ -1,10 +0,0 @@
import coreModule from 'grafana/app/core/core_module';
export function react2AngularDirective(name: string, component: any, options: any) {
coreModule.directive(name, [
'reactDirective',
reactDirective => {
return reactDirective(component, options);
},
]);
}

View File

@@ -38,30 +38,30 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
* ] * ]
*/ */
// Group history by itemid // Group history by itemid
const grouped_history = _.groupBy(history, 'itemid'); const grouped_history = _.groupBy(history, 'itemid');
const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
return _.map(grouped_history, (hist, itemid) => { return _.map(grouped_history, (hist, itemid) => {
const item = _.find(items, { 'itemid': itemid }) as any; const item = _.find(items, { itemid: itemid }) as any;
let alias = item.name; let alias = item.name;
// Add scopedVars for using in alias functions // Add scopedVars for using in alias functions
const scopedVars: any = { const scopedVars: any = {
'__zbx_item': { value: item.name }, __zbx_item: { value: item.name },
'__zbx_item_name': { value: item.name }, __zbx_item_name: { value: item.name },
'__zbx_item_key': { value: item.key_ }, __zbx_item_key: { value: item.key_ },
'__zbx_item_interval': { value: item.delay }, __zbx_item_interval: { value: item.delay },
}; };
if (_.keys(hosts).length > 0) { if (_.keys(hosts).length > 0) {
const host = _.find(hosts, { 'hostid': item.hostid }); const host = _.find(hosts, { hostid: item.hostid });
scopedVars['__zbx_host'] = { value: host.host }; scopedVars['__zbx_host'] = { value: host.host };
scopedVars['__zbx_host_name'] = { value: host.name }; scopedVars['__zbx_host_name'] = { value: host.name };
// Only add host when multiple hosts selected // Only add host when multiple hosts selected
if (_.keys(hosts).length > 1 && addHostName) { if (_.keys(hosts).length > 1 && addHostName) {
alias = host.name + ": " + alias; alias = host.name + ': ' + alias;
} }
} }
@@ -69,28 +69,33 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
target: alias, target: alias,
datapoints: _.map(hist, convertPointCallback), datapoints: _.map(hist, convertPointCallback),
scopedVars, scopedVars,
item item,
}; };
}); });
} }
export function seriesToDataFrame(timeseries, target: ZabbixMetricsQuery, valueMappings?: any[], fieldType?: FieldType): MutableDataFrame { export function seriesToDataFrame(
timeseries,
target: ZabbixMetricsQuery,
valueMappings?: any[],
fieldType?: FieldType
): MutableDataFrame {
const { datapoints, scopedVars, target: seriesName, item } = timeseries; const { datapoints, scopedVars, target: seriesName, item } = timeseries;
const timeFiled: Field = { const timeFiled: Field = {
name: TIME_SERIES_TIME_FIELD_NAME, name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time, type: FieldType.time,
config: { config: {
custom: {} custom: {},
}, },
values: new ArrayVector<number>(datapoints.map(p => p[c.DATAPOINT_TS])), values: new ArrayVector<number>(datapoints.map((p) => p[c.DATAPOINT_TS])),
}; };
let values: ArrayVector<number> | ArrayVector<string>; let values: ArrayVector<number> | ArrayVector<string>;
if (fieldType === FieldType.string) { if (fieldType === FieldType.string) {
values = new ArrayVector<string>(datapoints.map(p => p[c.DATAPOINT_VALUE])); values = new ArrayVector<string>(datapoints.map((p) => p[c.DATAPOINT_VALUE]));
} else { } else {
values = new ArrayVector<number>(datapoints.map(p => p[c.DATAPOINT_VALUE])); values = new ArrayVector<number>(datapoints.map((p) => p[c.DATAPOINT_VALUE]));
} }
const valueFiled: Field = { const valueFiled: Field = {
@@ -99,7 +104,7 @@ export function seriesToDataFrame(timeseries, target: ZabbixMetricsQuery, valueM
labels: {}, labels: {},
config: { config: {
displayNameFromDS: seriesName, displayNameFromDS: seriesName,
custom: {} custom: {},
}, },
values, values,
}; };
@@ -179,7 +184,7 @@ export function dataResponseToTimeSeries(response: DataFrameJSON[], items, reque
} }
const itemid = field.name; const itemid = field.name;
const item = _.find(items, { 'itemid': itemid }); const item = _.find(items, { itemid: itemid });
// Convert interval to nanoseconds in order to unmarshall it on the backend to time.Duration // Convert interval to nanoseconds in order to unmarshall it on the backend to time.Duration
let interval = request.intervalMs * 1000000; let interval = request.intervalMs * 1000000;
@@ -201,7 +206,7 @@ export function dataResponseToTimeSeries(response: DataFrameJSON[], items, reque
name: seriesName, name: seriesName,
item, item,
interval, interval,
} },
}; };
series.push(timeSeriesData); series.push(timeSeriesData);
@@ -263,7 +268,7 @@ export function itServiceResponseToTimeSeries(response: any, interval) {
name: s.target, name: s.target,
interval: null, interval: null,
item: {}, item: {},
} },
}; };
series.push(timeSeriesData); series.push(timeSeriesData);
@@ -277,13 +282,13 @@ export function isConvertibleToWide(data: DataFrame[]): boolean {
return false; return false;
} }
const first = data[0].fields.find(f => f.type === FieldType.time); const first = data[0].fields.find((f) => f.type === FieldType.time);
if (!first) { if (!first) {
return false; return false;
} }
for (let i = 1; i < data.length; i++) { for (let i = 1; i < data.length; i++) {
const timeField = data[i].fields.find(f => f.type === FieldType.time); const timeField = data[i].fields.find((f) => f.type === FieldType.time);
for (let j = 0; j < Math.min(data.length, 2); j++) { for (let j = 0; j < Math.min(data.length, 2); j++) {
if (timeField.values.get(j) !== first.values.get(j)) { if (timeField.values.get(j) !== first.values.get(j)) {
@@ -301,9 +306,9 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
} }
// Get oldest time stamp for all frames // Get oldest time stamp for all frames
let minTimestamp = data[0].fields.find(f => f.name === TIME_SERIES_TIME_FIELD_NAME).values.get(0); let minTimestamp = data[0].fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME).values.get(0);
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const timeField = data[i].fields.find(f => f.name === TIME_SERIES_TIME_FIELD_NAME); const timeField = data[i].fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
const firstTs = timeField.values.get(0); const firstTs = timeField.values.get(0);
if (firstTs < minTimestamp) { if (firstTs < minTimestamp) {
minTimestamp = firstTs; minTimestamp = firstTs;
@@ -312,8 +317,8 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const frame = data[i]; const frame = data[i];
const timeField = frame.fields.find(f => f.name === TIME_SERIES_TIME_FIELD_NAME); const timeField = frame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
const valueField = frame.fields.find(f => f.name === TIME_SERIES_VALUE_FIELD_NAME); const valueField = frame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
const firstTs = timeField.values.get(0); const firstTs = timeField.values.get(0);
if (firstTs > minTimestamp) { if (firstTs > minTimestamp) {
@@ -340,7 +345,7 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
export function convertToWide(data: MutableDataFrame[]): DataFrame[] { export function convertToWide(data: MutableDataFrame[]): DataFrame[] {
const maxLengthIndex = getLongestFrame(data); const maxLengthIndex = getLongestFrame(data);
const timeField = data[maxLengthIndex].fields.find(f => f.type === FieldType.time); const timeField = data[maxLengthIndex].fields.find((f) => f.type === FieldType.time);
if (!timeField) { if (!timeField) {
return []; return [];
} }
@@ -348,7 +353,7 @@ export function convertToWide(data: MutableDataFrame[]): DataFrame[] {
const fields: MutableField[] = [timeField]; const fields: MutableField[] = [timeField];
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const valueField = data[i].fields.find(f => f.name === TIME_SERIES_VALUE_FIELD_NAME); const valueField = data[i].fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
if (!valueField) { if (!valueField) {
continue; continue;
} }
@@ -363,7 +368,7 @@ export function convertToWide(data: MutableDataFrame[]): DataFrame[] {
} }
const frame: DataFrame = { const frame: DataFrame = {
name: "wide", name: 'wide',
fields, fields,
length: timeField.values.length, length: timeField.values.length,
}; };
@@ -375,7 +380,7 @@ function getLongestFrame(data: MutableDataFrame[]): number {
let maxLengthIndex = 0; let maxLengthIndex = 0;
let maxLength = 0; let maxLength = 0;
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const timeField = data[i].fields.find(f => f.type === FieldType.time); const timeField = data[i].fields.find((f) => f.type === FieldType.time);
if (timeField.values.length > maxLength) { if (timeField.values.length > maxLength) {
maxLength = timeField.values.length; maxLength = timeField.values.length;
maxLengthIndex = i; maxLengthIndex = i;
@@ -387,8 +392,8 @@ function getLongestFrame(data: MutableDataFrame[]): number {
function sortTimeseries(timeseries) { function sortTimeseries(timeseries) {
// Sort trend data, issue #202 // Sort trend data, issue #202
_.forEach(timeseries, series => { _.forEach(timeseries, (series) => {
series.datapoints = _.sortBy(series.datapoints, point => point[c.DATAPOINT_TS]); series.datapoints = _.sortBy(series.datapoints, (point) => point[c.DATAPOINT_TS]);
}); });
return timeseries; return timeseries;
} }
@@ -430,11 +435,9 @@ function handleHistoryAsTable(history, items, target) {
} }
let host: any = _.first(item.hosts); let host: any = _.first(item.hosts);
host = host ? host.name : ""; host = host ? host.name : '';
table.rows.push([ table.rows.push([host, item.name, item.key_, lastValue]);
host, item.name, item.key_, lastValue
]);
}); });
return table; return table;
@@ -448,10 +451,7 @@ function convertText(target, point) {
value = extractText(point.value, target.textFilter, target.useCaptureGroups); value = extractText(point.value, target.textFilter, target.useCaptureGroups);
} }
return [ return [value, point.clock * 1000 + Math.round(point.ns / 1000000)];
value,
point.clock * 1000 + Math.round(point.ns / 1000000)
];
} }
function extractText(str, pattern, useCaptureGroups) { function extractText(str, pattern, useCaptureGroups) {
@@ -464,31 +464,29 @@ function extractText(str, pattern, useCaptureGroups) {
return extractedValue[0]; return extractedValue[0];
} }
} }
return ""; return '';
} }
function handleSLAResponse(itservice, slaProperty, slaObject) { function handleSLAResponse(itservice, slaProperty, slaObject) {
const targetSLA = slaObject[itservice.serviceid].sla; const targetSLA = slaObject[itservice.serviceid].sla;
if (slaProperty.property === 'status') { if (slaProperty === 'status') {
const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10); const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10);
return { return {
target: itservice.name + ' ' + slaProperty.name, target: itservice.name + ' ' + slaProperty,
datapoints: [ datapoints: [[targetStatus, targetSLA[0].to * 1000]],
[targetStatus, targetSLA[0].to * 1000]
]
}; };
} else { } else {
let i; let i;
const slaArr = []; const slaArr = [];
for (i = 0; i < targetSLA.length; i++) { for (i = 0; i < targetSLA.length; i++) {
if (i === 0) { if (i === 0) {
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]); slaArr.push([targetSLA[i][slaProperty], targetSLA[i].from * 1000]);
} }
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].to * 1000]); slaArr.push([targetSLA[i][slaProperty], targetSLA[i].to * 1000]);
} }
return { return {
target: itservice.name + ' ' + slaProperty.name, target: itservice.name + ' ' + slaProperty,
datapoints: slaArr datapoints: slaArr,
}; };
} }
} }
@@ -499,13 +497,11 @@ function handleTriggersResponse(triggers, groups, timeRange) {
try { try {
triggersCount = Number(triggers); triggersCount = Number(triggers);
} catch (err) { } catch (err) {
console.log("Error when handling triggers count: ", err); console.log('Error when handling triggers count: ', err);
} }
return { return {
target: "triggers count", target: 'triggers count',
datapoints: [ datapoints: [[triggersCount, timeRange[1] * 1000]],
[triggersCount, timeRange[1] * 1000]
]
}; };
} else { } else {
const stats = getTriggerStats(triggers); const stats = getTriggerStats(triggers);
@@ -517,7 +513,10 @@ function handleTriggersResponse(triggers, groups, timeRange) {
}); });
_.each(stats, (severity_stats, group) => { _.each(stats, (severity_stats, group) => {
if (_.includes(groupNames, group)) { if (_.includes(groupNames, group)) {
let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]); let row = _.map(
_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']),
(s) => s[1]
);
row = _.concat([group], ...row); row = _.concat([group], ...row);
table.rows.push(row); table.rows.push(row);
} }
@@ -543,38 +542,32 @@ function getTriggerStats(triggers) {
function convertHistoryPoint(point) { function convertHistoryPoint(point) {
// Value must be a number for properly work // Value must be a number for properly work
return [ return [Number(point.value), point.clock * 1000 + Math.round(point.ns / 1000000)];
Number(point.value),
point.clock * 1000 + Math.round(point.ns / 1000000)
];
} }
function convertTrendPoint(valueType, point) { function convertTrendPoint(valueType, point) {
let value; let value;
switch (valueType) { switch (valueType) {
case "min": case 'min':
value = point.value_min; value = point.value_min;
break; break;
case "max": case 'max':
value = point.value_max; value = point.value_max;
break; break;
case "avg": case 'avg':
value = point.value_avg; value = point.value_avg;
break; break;
case "sum": case 'sum':
value = point.value_avg * point.num; value = point.value_avg * point.num;
break; break;
case "count": case 'count':
value = point.num; value = point.num;
break; break;
default: default:
value = point.value_avg; value = point.value_avg;
} }
return [ return [Number(value), point.clock * 1000];
Number(value),
point.clock * 1000
];
} }
export default { export default {

View File

@@ -13,6 +13,10 @@ jest.mock('@grafana/runtime', () => ({
}, },
}), { virtual: true }); }), { virtual: true });
jest.mock('../components/AnnotationQueryEditor', () => ({
AnnotationQueryEditor: () => {},
}));
describe('ZabbixDatasource', () => { describe('ZabbixDatasource', () => {
let ctx = {}; let ctx = {};

View File

@@ -1,4 +1,4 @@
import { DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from "@grafana/data"; import { DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data';
export interface ZabbixDSOptions extends DataSourceJsonData { export interface ZabbixDSOptions extends DataSourceJsonData {
username: string; username: string;
@@ -34,24 +34,26 @@ export interface ZabbixConnectionTestQuery {
} }
export interface ZabbixMetricsQuery extends DataQuery { export interface ZabbixMetricsQuery extends DataQuery {
triggers: { minSeverity: string; acknowledged: boolean; count: number; };
queryType: string; queryType: string;
datasourceId: number; datasourceId?: number;
group: { filter: string; name?: string; }; group?: { filter: string; name?: string };
host: { filter: string; name?: string; }; host?: { filter: string; name?: string };
application: { filter: string; name?: string; }; application?: { filter: string; name?: string };
itemTag: { filter: string; name?: string; }; itemTag?: { filter: string; name?: string };
item: { filter: string; name?: string; }; item?: { filter: string; name?: string };
textFilter: string; textFilter?: string;
mode: number; mode?: number;
itemids: number[]; itemids?: string;
useCaptureGroups: boolean; useCaptureGroups?: boolean;
proxy?: { filter: string; }; proxy?: { filter: string };
trigger?: { filter: string; }; trigger?: { filter: string };
itServiceFilter?: string; itServiceFilter?: string;
tags?: { filter: string; }; slaProperty?: any;
functions: ZabbixMetricFunction[]; slaInterval?: string;
options: ZabbixQueryOptions; tags?: { filter: string };
triggers?: { minSeverity: number; acknowledged: number; count: boolean };
functions?: MetricFunc[];
options?: ZabbixQueryOptions;
// Problems // Problems
showProblems?: ShowProblemTypes; showProblems?: ShowProblemTypes;
// Deprecated // Deprecated
@@ -73,14 +75,44 @@ export interface ZabbixQueryOptions {
limit?: number; limit?: number;
useTimeRange?: boolean; useTimeRange?: boolean;
severities?: number[]; severities?: number[];
// Annotations
showOkEvents?: boolean;
hideAcknowledged?: boolean;
showHostname?: boolean;
} }
export interface ZabbixMetricFunction { export interface MetricFunc {
name: string; text: string;
params: any; params: Array<string | number>;
def: { name: string; params: any; }; def: FuncDef;
added?: boolean;
} }
export interface FuncDef {
name: string;
params: ParamDef[];
defaultParams: Array<string | number>;
category?: string;
shortName?: any;
fake?: boolean;
version?: string;
description?: string;
/**
* True if the function was not found on the list of available function descriptions.
*/
unknown?: boolean;
}
export type ParamDef = {
name: string;
type: string;
options?: Array<string | number>;
multiple?: boolean;
optional?: boolean;
version?: string;
};
// The paths of these files have moved around in Grafana and they don't resolve properly // The paths of these files have moved around in Grafana and they don't resolve properly
// either. Safer not to bother trying to import them just for type hinting. // either. Safer not to bother trying to import them just for type hinting.

View File

@@ -20,7 +20,6 @@ export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\
* @return {string} expanded name, ie "CPU system time" * @return {string} expanded name, ie "CPU system time"
*/ */
export function expandItemName(name: string, key: string): string { export function expandItemName(name: string, key: string): string {
// extract params from key: // extract params from key:
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"] // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
const key_params_str = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']')); const key_params_str = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']'));
@@ -34,7 +33,7 @@ export function expandItemName(name: string, key: string): string {
} }
export function expandItems(items) { export function expandItems(items) {
_.forEach(items, item => { _.forEach(items, (item) => {
item.item = item.name; item.item = item.name;
item.name = expandItemName(item.item, item.key_); item.name = expandItemName(item.item, item.key_);
return item; return item;
@@ -49,7 +48,7 @@ function splitKeyParams(paramStr) {
const split_symbol = ','; const split_symbol = ',';
let param = ''; let param = '';
_.forEach(paramStr, symbol => { _.forEach(paramStr, (symbol) => {
if (symbol === '"' && in_array) { if (symbol === '"' && in_array) {
param += symbol; param += symbol;
} else if (symbol === '"' && quoted) { } else if (symbol === '"' && quoted) {
@@ -81,14 +80,14 @@ export function containsMacro(itemName) {
export function replaceMacro(item, macros, isTriggerItem?) { export function replaceMacro(item, macros, isTriggerItem?) {
let itemName = isTriggerItem ? item.url : item.name; let itemName = isTriggerItem ? item.url : item.name;
const item_macros = itemName.match(MACRO_PATTERN); const item_macros = itemName.match(MACRO_PATTERN);
_.forEach(item_macros, macro => { _.forEach(item_macros, (macro) => {
const host_macros = _.filter(macros, m => { const host_macros = _.filter(macros, (m) => {
if (m.hostid) { if (m.hostid) {
if (isTriggerItem) { if (isTriggerItem) {
// Trigger item can have multiple hosts // Trigger item can have multiple hosts
// Check all trigger host ids against macro host id // Check all trigger host ids against macro host id
let hostIdFound = false; let hostIdFound = false;
_.forEach(item.hosts, h => { _.forEach(item.hosts, (h) => {
if (h.hostid === m.hostid) { if (h.hostid === m.hostid) {
hostIdFound = true; hostIdFound = true;
} }
@@ -116,7 +115,7 @@ export function replaceMacro(item, macros, isTriggerItem?) {
} }
function escapeMacro(macro) { function escapeMacro(macro) {
macro = macro.replace(/\$/, '\\\$'); macro = macro.replace(/\$/, '\\$');
return macro; return macro;
} }
@@ -125,7 +124,7 @@ export function parseLegacyVariableQuery(query: string): VariableQuery {
const parts = []; const parts = [];
// Split query. Query structure: group.host.app.item // Split query. Query structure: group.host.app.item
_.each(splitTemplateQuery(query), part => { _.each(splitTemplateQuery(query), (part) => {
// Replace wildcard to regex // Replace wildcard to regex
if (part === '*') { if (part === '*') {
part = '/.*/'; part = '/.*/';
@@ -176,7 +175,7 @@ export function splitTemplateQuery(query) {
if (isContainsBraces(query)) { if (isContainsBraces(query)) {
const result = query.match(splitPattern); const result = query.match(splitPattern);
split = _.map(result, part => { split = _.map(result, (part) => {
return _.trim(part, '{}'); return _.trim(part, '{}');
}); });
} else { } else {
@@ -201,7 +200,7 @@ export function isRegex(str) {
export function isTemplateVariable(str, templateVariables) { export function isTemplateVariable(str, templateVariables) {
const variablePattern = /^\$\w+/; const variablePattern = /^\$\w+/;
if (variablePattern.test(str)) { if (variablePattern.test(str)) {
const variables = _.map(templateVariables, variable => { const variables = _.map(templateVariables, (variable) => {
return '$' + variable.name; return '$' + variable.name;
}); });
return _.includes(variables, str); return _.includes(variables, str);
@@ -225,7 +224,7 @@ export function getRangeScopedVars(range) {
export function buildRegex(str) { export function buildRegex(str) {
const matches = str.match(regexPattern); const matches = str.match(regexPattern);
const pattern = matches[1]; const pattern = matches[1];
const flags = matches[2] !== "" ? matches[2] : undefined; const flags = matches[2] !== '' ? matches[2] : undefined;
return new RegExp(pattern, flags); return new RegExp(pattern, flags);
} }
@@ -260,7 +259,7 @@ export function parseInterval(interval: string): number {
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g; const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
const momentInterval: any[] = intervalPattern.exec(interval); const momentInterval: any[] = intervalPattern.exec(interval);
const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]); const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]);
return (duration.valueOf() as number); return duration.valueOf() as number;
} }
export function parseTimeShiftInterval(interval) { export function parseTimeShiftInterval(interval) {
@@ -285,15 +284,30 @@ export function parseTimeShiftInterval(interval) {
*/ */
export function formatAcknowledges(acknowledges) { export function formatAcknowledges(acknowledges) {
if (acknowledges.length) { if (acknowledges.length) {
let formatted_acknowledges = '<br><br>Acknowledges:<br><table><tr><td><b>Time</b></td>' let formatted_acknowledges =
+ '<td><b>User</b></td><td><b>Comments</b></td></tr>'; '<br><br>Acknowledges:<br><table><tr><td><b>Time</b></td>' + '<td><b>User</b></td><td><b>Comments</b></td></tr>';
_.each(_.map(acknowledges, ack => { _.each(
const timestamp = moment.unix(ack.clock); _.map(acknowledges, (ack) => {
return '<tr><td><i>' + timestamp.format("DD MMM YYYY HH:mm:ss") + '</i></td><td>' + ack.alias const timestamp = moment.unix(ack.clock);
+ ' (' + ack.name + ' ' + ack.surname + ')' + '</td><td>' + ack.message + '</td></tr>'; return (
}), ack => { '<tr><td><i>' +
formatted_acknowledges = formatted_acknowledges.concat(ack); timestamp.format('DD MMM YYYY HH:mm:ss') +
}); '</i></td><td>' +
ack.alias +
' (' +
ack.name +
' ' +
ack.surname +
')' +
'</td><td>' +
ack.message +
'</td></tr>'
);
}),
(ack) => {
formatted_acknowledges = formatted_acknowledges.concat(ack);
}
);
formatted_acknowledges = formatted_acknowledges.concat('</table>'); formatted_acknowledges = formatted_acknowledges.concat('</table>');
return formatted_acknowledges; return formatted_acknowledges;
} else { } else {
@@ -307,7 +321,7 @@ export function convertToZabbixAPIUrl(url) {
if (url.match(zabbixAPIUrlPattern)) { if (url.match(zabbixAPIUrlPattern)) {
return url; return url;
} else { } else {
return url.replace(trimSlashPattern, "$1"); return url.replace(trimSlashPattern, '$1');
} }
} }
@@ -319,14 +333,16 @@ export function callOnce(func, promiseKeeper) {
return function () { return function () {
if (!promiseKeeper) { if (!promiseKeeper) {
promiseKeeper = Promise.resolve( promiseKeeper = Promise.resolve(
func.apply(this, arguments) func
.then(result => { .apply(this, arguments)
promiseKeeper = null; .then((result) => {
return result; promiseKeeper = null;
}).catch(err => { return result;
promiseKeeper = null; })
throw err; .catch((err) => {
}) promiseKeeper = null;
throw err;
})
); );
} }
return promiseKeeper; return promiseKeeper;
@@ -426,19 +442,19 @@ export function mustArray(result: any): any[] {
const getUnitsMap = () => ({ const getUnitsMap = () => ({
'%': 'percent', '%': 'percent',
'b': 'decbits', // bits(SI) b: 'decbits', // bits(SI)
'bps': 'bps', // bits/sec(SI) bps: 'bps', // bits/sec(SI)
'B': 'bytes', // bytes(IEC) B: 'bytes', // bytes(IEC)
'Bps': 'binBps', // bytes/sec(IEC) Bps: 'binBps', // bytes/sec(IEC)
// 'unixtime': 'dateTimeAsSystem', // 'unixtime': 'dateTimeAsSystem',
'uptime': 'dtdhms', uptime: 'dtdhms',
'qps': 'qps', // requests/sec (rps) qps: 'qps', // requests/sec (rps)
'iops': 'iops', // I/O ops/sec (iops) iops: 'iops', // I/O ops/sec (iops)
'Hz': 'hertz', // Hertz (1/s) Hz: 'hertz', // Hertz (1/s)
'V': 'volt', // Volt (V) V: 'volt', // Volt (V)
'C': 'celsius', // Celsius (°C) C: 'celsius', // Celsius (°C)
'RPM': 'rotrpm', // Revolutions per minute (rpm) RPM: 'rotrpm', // Revolutions per minute (rpm)
'dBm': 'dBm', // Decibel-milliwatt (dBm) dBm: 'dBm', // Decibel-milliwatt (dBm)
}); });
const getKnownGrafanaUnits = () => { const getKnownGrafanaUnits = () => {
@@ -466,7 +482,7 @@ export function convertZabbixUnit(zabbixUnit: string): string {
export function getValueMapping(item, valueMappings: any[]): ValueMapping[] | null { export function getValueMapping(item, valueMappings: any[]): ValueMapping[] | null {
const { valuemapid } = item; const { valuemapid } = item;
const mapping = valueMappings?.find(m => m.valuemapid === valuemapid); const mapping = valueMappings?.find((m) => m.valuemapid === valuemapid);
if (!mapping) { if (!mapping) {
return null; return null;
} }
@@ -478,12 +494,33 @@ export function getValueMapping(item, valueMappings: any[]): ValueMapping[] | nu
options: { options: {
value: m.value, value: m.value,
text: m.newvalue, text: m.newvalue,
} },
}; };
return valueMapping; return valueMapping;
}); });
} }
export function isProblemsDataFrame(data: DataFrame): boolean { export function isProblemsDataFrame(data: DataFrame): boolean {
return data.fields.length && data.fields[0].type === FieldType.other && data.fields[0].config.custom['type'] === 'problems'; return (
data.fields.length && data.fields[0].type === FieldType.other && data.fields[0].config.custom['type'] === 'problems'
);
}
// Swap n and k elements.
export function swap<T>(list: Array<T>, n: number, k: number): Array<T> {
if (list === null || list.length < 2 || k > list.length - 1 || k < 0 || n > list.length - 1 || n < 0) {
return list;
}
const newList: Array<T> = new Array(list.length);
for (let i = 0; i < list.length; i++) {
if (i === n) {
newList[i] = list[k];
} else if (i === k) {
newList[i] = list[n];
} else {
newList[i] = list[i];
}
}
return newList;
} }

View File

@@ -1,7 +1,23 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { cx, css } from '@emotion/css'; import { cx, css } from '@emotion/css';
import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants'; import {
import { Button, VerticalGroup, Spinner, Modal, Checkbox, RadioButtonGroup, stylesFactory, withTheme, Themeable, TextArea } from '@grafana/ui'; 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,
Checkbox,
RadioButtonGroup,
stylesFactory,
withTheme,
Themeable,
TextArea,
} from '@grafana/ui';
import { FAIcon } from '../../components'; import { FAIcon } from '../../components';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
@@ -37,12 +53,12 @@ export interface AckProblemData {
} }
const severityOptions = [ const severityOptions = [
{value: 0, label: 'Not classified'}, { value: 0, label: 'Not classified' },
{value: 1, label: 'Information'}, { value: 1, label: 'Information' },
{value: 2, label: 'Warning'}, { value: 2, label: 'Warning' },
{value: 3, label: 'Average'}, { value: 3, label: 'Average' },
{value: 4, label: 'High'}, { value: 4, label: 'High' },
{value: 5, label: 'Disaster'} { value: 5, label: 'Disaster' },
]; ];
export class AckModalUnthemed extends PureComponent<Props, State> { export class AckModalUnthemed extends PureComponent<Props, State> {
@@ -67,7 +83,7 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({ value: event.target.value, error: false }); this.setState({ value: event.target.value, error: false });
} };
handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') { if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') {
@@ -75,32 +91,32 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
} else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') { } else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') {
this.dismiss(); this.dismiss();
} }
} };
handleBackdropClick = () => { handleBackdropClick = () => {
this.dismiss(); this.dismiss();
} };
onAcknowledgeToggle = () => { onAcknowledgeToggle = () => {
this.setState({ acknowledge: !this.state.acknowledge, error: false }); this.setState({ acknowledge: !this.state.acknowledge, error: false });
} };
onChangeSeverityToggle = () => { onChangeSeverityToggle = () => {
this.setState({ changeSeverity: !this.state.changeSeverity, error: false }); this.setState({ changeSeverity: !this.state.changeSeverity, error: false });
} };
onCloseProblemToggle = () => { onCloseProblemToggle = () => {
this.setState({ closeProblem: !this.state.closeProblem, error: false }); this.setState({ closeProblem: !this.state.closeProblem, error: false });
} };
onChangeSelectedSeverity = v => { onChangeSelectedSeverity = (v) => {
this.setState({ selectedSeverity: v }); this.setState({ selectedSeverity: v });
}; };
dismiss = () => { dismiss = () => {
this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false }); this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false });
this.props.onDismiss(); this.props.onDismiss();
} };
submit = () => { submit = () => {
const { acknowledge, changeSeverity, closeProblem } = this.state; const { acknowledge, changeSeverity, closeProblem } = this.state;
@@ -109,7 +125,7 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
if (!this.state.value && !actionSelected) { if (!this.state.value && !actionSelected) {
return this.setState({ return this.setState({
error: true, error: true,
errorMessage: 'Enter message text or select an action' errorMessage: 'Enter message text or select an action',
}); });
} }
@@ -132,51 +148,54 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
} }
ackData.action = action; ackData.action = action;
this.props.onSubmit(ackData).then(() => { this.props
this.dismiss(); .onSubmit(ackData)
}).catch(err => { .then(() => {
const errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || ''; this.dismiss();
this.setState({ })
ackError: errorMessage, .catch((err) => {
loading: false, const errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || '';
this.setState({
ackError: errorMessage,
loading: false,
});
}); });
}); };
}
renderActions() { renderActions() {
const { canClose } = this.props; const { canClose } = this.props;
const actions = [ const actions = [
<Checkbox translate="" key="ack" label="Acknowledge" value={this.state.acknowledge} onChange={this.onAcknowledgeToggle} />, <Checkbox key="ack" label="Acknowledge" value={this.state.acknowledge} onChange={this.onAcknowledgeToggle} />,
<Checkbox <Checkbox
translate=""
key="change-severity" key="change-severity"
label="Change severity" label="Change severity"
description="" description=""
value={this.state.changeSeverity} value={this.state.changeSeverity}
onChange={this.onChangeSeverityToggle} onChange={this.onChangeSeverityToggle}
/>, />,
this.state.changeSeverity && this.state.changeSeverity && (
<RadioButtonGroup <RadioButtonGroup
key="severity" key="severity"
size="sm" size="sm"
options={severityOptions} options={severityOptions}
value={this.state.selectedSeverity} value={this.state.selectedSeverity}
onChange={this.onChangeSelectedSeverity} onChange={this.onChangeSelectedSeverity}
/>, />
canClose && ),
canClose && (
<Checkbox <Checkbox
translate=""
key="close" key="close"
label="Close problem" label="Close problem"
disabled={!canClose} disabled={!canClose}
value={this.state.closeProblem} value={this.state.closeProblem}
onChange={this.onCloseProblemToggle} onChange={this.onCloseProblemToggle}
/>, />
),
]; ];
// <VerticalGroup /> doesn't handle empty elements properly, so don't return it // <VerticalGroup /> doesn't handle empty elements properly, so don't return it
return actions.filter(e => e); return actions.filter((e) => e);
} }
render() { render() {
@@ -204,8 +223,8 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
> >
<div className={inputGroupClass}> <div className={inputGroupClass}>
<label className="gf-form-hint"> <label className="gf-form-hint">
<TextArea className={inputClass} <TextArea
translate="" className={inputClass}
type="text" type="text"
name="message" name="message"
placeholder="Message" placeholder="Message"
@@ -213,30 +232,30 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
autoFocus={true} autoFocus={true}
value={this.state.value} value={this.state.value}
onChange={this.handleChange} onChange={this.handleChange}
onKeyDown={this.handleKeyPress}> onKeyDown={this.handleKeyPress}
</TextArea> ></TextArea>
<small className={inputHintClass}>Press Enter to submit</small> <small className={inputHintClass}>Press Enter to submit</small>
{this.state.error && {this.state.error && <small className={inputErrorClass}>{this.state.errorMessage}</small>}
<small className={inputErrorClass}>{this.state.errorMessage}</small>
}
</label> </label>
</div> </div>
<div className="gf-form"> <div className="gf-form">
<VerticalGroup> <VerticalGroup>{this.renderActions()}</VerticalGroup>
{this.renderActions()}
</VerticalGroup>
</div> </div>
{this.state.ackError && {this.state.ackError && (
<div className="gf-form ack-request-error"> <div className="gf-form ack-request-error">
<span className={styles.ackError}>{this.state.ackError}</span> <span className={styles.ackError}>{this.state.ackError}</span>
</div> </div>
} )}
<div className="gf-form-button-row text-center"> <div className="gf-form-button-row text-center">
<Button variant="primary" onClick={this.submit}>Update</Button> <Button variant="primary" onClick={this.submit}>
<Button variant="secondary" onClick={this.dismiss}>Cancel</Button> Update
</Button>
<Button variant="secondary" onClick={this.dismiss}>
Cancel
</Button>
</div> </div>
</Modal> </Modal>
); );

8570
yarn.lock

File diff suppressed because it is too large Load Diff