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:
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -2,8 +2,6 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
@@ -16,12 +14,12 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: "16.x"
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Setup Go environment
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
go-version: '1.17'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@@ -61,12 +59,12 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: "16.x"
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Setup Go environment
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
go-version: '1.17'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@@ -105,12 +103,12 @@ jobs:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: "16.x"
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Setup Go environment
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.17"
|
||||
go-version: '1.17'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
19
package.json
19
package.json
@@ -30,18 +30,18 @@
|
||||
"@babel/preset-react": "7.6.3",
|
||||
"@emotion/css": "11.1.3",
|
||||
"@emotion/react": "11.1.5",
|
||||
"@grafana/data": "^8.3.6",
|
||||
"@grafana/runtime": "^8.3.6",
|
||||
"@grafana/toolkit": "^8.3.6",
|
||||
"@grafana/ui": "^8.3.6",
|
||||
"@grafana/data": "9.1.2",
|
||||
"@grafana/runtime": "9.1.2",
|
||||
"@grafana/toolkit": "9.1.2",
|
||||
"@grafana/ui": "9.1.2",
|
||||
"@popperjs/core": "2.4.0",
|
||||
"@types/classnames": "2.2.9",
|
||||
"@types/grafana": "github:CorpGlory/types-grafana",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/jquery": "3.3.32",
|
||||
"@types/lodash": "4.14.161",
|
||||
"@types/react": "16.8.16",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react": "17.0.42",
|
||||
"@types/react-dom": "17.0.14",
|
||||
"@types/react-transition-group": "4.2.4",
|
||||
"axios": "^0.21.1",
|
||||
"babel-jest": "24.8.0",
|
||||
@@ -69,12 +69,13 @@
|
||||
"ng-annotate-webpack-plugin": "0.3.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.12.0",
|
||||
"react-dom": "16.12.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-popper": "^2.2.3",
|
||||
"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-use": "17.4.0",
|
||||
"rst2html": "github:thoward/rst2html#990cb89",
|
||||
"rxjs": "6.6.3",
|
||||
"sass-loader": "10.2.1",
|
||||
|
||||
141
src/components/MetricPicker/MetricPicker.tsx
Normal file
141
src/components/MetricPicker/MetricPicker.tsx
Normal 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};
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
138
src/components/MetricPicker/MetricPickerMenu.tsx
Normal file
138
src/components/MetricPicker/MetricPickerMenu.tsx
Normal 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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
2
src/components/MetricPicker/constants.ts
Normal file
2
src/components/MetricPicker/constants.ts
Normal 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;
|
||||
@@ -4,3 +4,4 @@ export { AckButton } from './AckButton/AckButton';
|
||||
export { ExploreButton } from './ExploreButton/ExploreButton';
|
||||
export { ExecScriptButton } from './ExecScriptButton/ExecScriptButton';
|
||||
export { ModalController } from './Modal/ModalController';
|
||||
export { MetricPicker } from './MetricPicker/MetricPicker';
|
||||
|
||||
@@ -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 + "')",
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
195
src/datasource-zabbix/components/AnnotationQueryEditor.tsx
Normal file
195
src/datasource-zabbix/components/AnnotationQueryEditor.tsx
Normal 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>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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)};
|
||||
`,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
`,
|
||||
});
|
||||
@@ -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),
|
||||
}),
|
||||
});
|
||||
58
src/datasource-zabbix/components/FunctionEditor/helpers.ts
Normal file
58
src/datasource-zabbix/components/FunctionEditor/helpers.ts
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
212
src/datasource-zabbix/components/QueryEditor.tsx
Normal file
212
src/datasource-zabbix/components/QueryEditor.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)};
|
||||
`,
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
13
src/datasource-zabbix/components/QueryEditor/utils.ts
Normal file
13
src/datasource-zabbix/components/QueryEditor/utils.ts
Normal 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}`,
|
||||
}));
|
||||
};
|
||||
@@ -35,10 +35,10 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
||||
this.state = {
|
||||
selectedQueryType,
|
||||
legacyQuery: this.props.query,
|
||||
...query
|
||||
...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);
|
||||
this.state = {
|
||||
...this.defaults,
|
||||
@@ -51,7 +51,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -108,70 +108,68 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
||||
<InlineFormLabel width={10}>Group</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={group}
|
||||
onChange={evt => this.handleQueryUpdate(evt, 'group')}
|
||||
onChange={(evt) => this.handleQueryUpdate(evt, 'group')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
{selectedQueryType.value !== VariableQueryTypes.Group &&
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Host</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={host}
|
||||
onChange={evt => this.handleQueryUpdate(evt, 'host')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{selectedQueryType.value !== VariableQueryTypes.Group && (
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Host</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={host}
|
||||
onChange={(evt) => this.handleQueryUpdate(evt, 'host')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(selectedQueryType.value === VariableQueryTypes.Application ||
|
||||
selectedQueryType.value === VariableQueryTypes.ItemTag ||
|
||||
selectedQueryType.value === VariableQueryTypes.Item ||
|
||||
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
|
||||
<div className="gf-form-inline">
|
||||
{supportsItemTags && (
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Item tag</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={itemTag}
|
||||
onChange={evt => this.handleQueryUpdate(evt, 'itemTag')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!supportsItemTags && (
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Application</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={application}
|
||||
onChange={evt => this.handleQueryUpdate(evt, 'application')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(selectedQueryType.value === VariableQueryTypes.Item ||
|
||||
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Item</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={item}
|
||||
onChange={evt => this.handleQueryUpdate(evt, 'item')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
|
||||
<div className="gf-form-inline">
|
||||
{supportsItemTags && (
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Item tag</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={itemTag}
|
||||
onChange={(evt) => this.handleQueryUpdate(evt, 'itemTag')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!supportsItemTags && (
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Application</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={application}
|
||||
onChange={(evt) => this.handleQueryUpdate(evt, 'application')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(selectedQueryType.value === VariableQueryTypes.Item ||
|
||||
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
|
||||
<div className="gf-form max-width-30">
|
||||
<InlineFormLabel width={10}>Item</InlineFormLabel>
|
||||
<ZabbixInput
|
||||
value={item}
|
||||
onChange={(evt) => this.handleQueryUpdate(evt, 'item')}
|
||||
onBlur={this.handleQueryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{legacyQuery &&
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel width={10} tooltip="Original query string, read-only">Legacy Query</InlineFormLabel>
|
||||
<Input
|
||||
css=""
|
||||
value={legacyQuery}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{legacyQuery && (
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel width={10} tooltip="Original query string, read-only">
|
||||
Legacy Query
|
||||
</InlineFormLabel>
|
||||
<Input value={legacyQuery} readOnly={true} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { EventsWithValidation, ValidationEvents, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { EventsWithValidation, ValidationEvents, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { isRegex, variableRegex } from '../utils';
|
||||
|
||||
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 getStyles = (theme: GrafanaTheme) => ({
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
inputRegex: css`
|
||||
color: ${theme.palette.orange}
|
||||
color: ${theme.colors.warning.main};
|
||||
`,
|
||||
inputVariable: css`
|
||||
color: ${theme.colors.textBlue}
|
||||
color: ${theme.colors.action.focus};
|
||||
`,
|
||||
});
|
||||
|
||||
const zabbixInputValidationEvents: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
{
|
||||
rule: value => {
|
||||
rule: (value) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const zabbixInputValidationEvents: ValidationEvents = {
|
||||
errorMessage: 'Not a valid regex',
|
||||
},
|
||||
{
|
||||
rule: value => {
|
||||
rule: (value) => {
|
||||
if (value === '*') {
|
||||
return false;
|
||||
}
|
||||
@@ -47,8 +47,7 @@ const zabbixInputValidationEvents: ValidationEvents = {
|
||||
};
|
||||
|
||||
export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
let inputClass = styles.inputRegex;
|
||||
if (variablePattern.test(value as string)) {
|
||||
@@ -57,12 +56,5 @@ export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProp
|
||||
inputClass = styles.inputRegex;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={inputClass}
|
||||
value={value}
|
||||
validationEvents={zabbixInputValidationEvents}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
return <Input className={inputClass} value={value} validationEvents={zabbixInputValidationEvents} {...restProps} />;
|
||||
};
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
DataSourceInstanceSettings,
|
||||
FieldType,
|
||||
isDataFrame,
|
||||
LoadingState
|
||||
LoadingState,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
|
||||
|
||||
export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> {
|
||||
name: string;
|
||||
@@ -53,6 +55,11 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
this.templateSrv = templateSrv;
|
||||
this.enableDebugLog = config.buildInfo.env === 'development';
|
||||
|
||||
this.annotations = {
|
||||
QueryEditor: AnnotationQueryEditor,
|
||||
prepareAnnotation: migrations.prepareAnnotation,
|
||||
};
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
|
||||
|
||||
@@ -108,7 +115,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
*/
|
||||
query(request: DataQueryRequest<ZabbixMetricsQuery>) {
|
||||
// Migrate old targets
|
||||
const requestTargets = request.targets.map(t => {
|
||||
const requestTargets = request.targets.map((t) => {
|
||||
// Prevent changes of original object
|
||||
const target = _.cloneDeep(t);
|
||||
return migrations.migrate(target);
|
||||
@@ -117,11 +124,16 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
const backendResponsePromise = this.backendQuery({ ...request, targets: requestTargets });
|
||||
const dbConnectionResponsePromise = this.dbConnectionQuery({ ...request, targets: requestTargets });
|
||||
const frontendResponsePromise = this.frontendQuery({ ...request, targets: requestTargets });
|
||||
const annotationResposePromise = this.annotationRequest({ ...request, targets: requestTargets });
|
||||
|
||||
return Promise.all([backendResponsePromise, dbConnectionResponsePromise, frontendResponsePromise])
|
||||
.then(rsp => {
|
||||
return Promise.all([
|
||||
backendResponsePromise,
|
||||
dbConnectionResponsePromise,
|
||||
frontendResponsePromise,
|
||||
annotationResposePromise,
|
||||
]).then((rsp) => {
|
||||
// Merge backend and frontend queries results
|
||||
const [backendRes, dbConnectionRes, frontendRes] = rsp;
|
||||
const [backendRes, dbConnectionRes, frontendRes, annotationRes] = rsp;
|
||||
if (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);
|
||||
}
|
||||
|
||||
if (annotationRes.data) {
|
||||
backendRes.data = backendRes.data.concat(annotationRes.data);
|
||||
}
|
||||
|
||||
return {
|
||||
data: backendRes.data,
|
||||
state: LoadingState.Done,
|
||||
@@ -144,21 +160,23 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
// Add range variables
|
||||
request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
|
||||
|
||||
const queries = _.compact(targets.map((query) => {
|
||||
// Don't request for hidden targets
|
||||
if (query.hide) {
|
||||
return null;
|
||||
}
|
||||
const queries = _.compact(
|
||||
targets.map((query) => {
|
||||
// Don't request for hidden targets
|
||||
if (query.hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.replaceTargetVariables(query, request);
|
||||
this.replaceTargetVariables(query, request);
|
||||
|
||||
return {
|
||||
...query,
|
||||
datasourceId: this.datasourceId,
|
||||
intervalMs,
|
||||
maxDataPoints,
|
||||
};
|
||||
}));
|
||||
return {
|
||||
...query,
|
||||
datasourceId: this.datasourceId,
|
||||
intervalMs,
|
||||
maxDataPoints,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Return early if no queries exist
|
||||
if (!queries.length) {
|
||||
@@ -175,12 +193,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
|
||||
let rsp: any;
|
||||
try {
|
||||
rsp = await getBackendSrv().fetch({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: body,
|
||||
requestId,
|
||||
}).toPromise();
|
||||
rsp = await getBackendSrv()
|
||||
.fetch({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: body,
|
||||
requestId,
|
||||
})
|
||||
.toPromise();
|
||||
} catch (err) {
|
||||
return toDataQueryResponse(err);
|
||||
}
|
||||
@@ -198,8 +218,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
}
|
||||
|
||||
async frontendQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> {
|
||||
const frontendTargets = request.targets.filter(t => !(this.isBackendTarget(t) || this.isDBConnectionTarget(t)));
|
||||
const promises = _.map(frontendTargets, target => {
|
||||
const frontendTargets = request.targets.filter((t) => !(this.isBackendTarget(t) || this.isDBConnectionTarget(t)));
|
||||
const promises = _.map(frontendTargets, (target) => {
|
||||
// Don't request for hidden targets
|
||||
if (target.hide) {
|
||||
return [];
|
||||
@@ -233,50 +253,52 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
|
||||
// Data for panel (all targets)
|
||||
return Promise.all(_.flatten(promises))
|
||||
.then(_.flatten)
|
||||
.then(data => {
|
||||
if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) {
|
||||
data = responseHandler.alignFrames(data);
|
||||
if (responseHandler.isConvertibleToWide(data)) {
|
||||
console.log('Converting response to the wide format');
|
||||
data = responseHandler.convertToWide(data);
|
||||
.then(_.flatten)
|
||||
.then((data) => {
|
||||
if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) {
|
||||
data = responseHandler.alignFrames(data);
|
||||
if (responseHandler.isConvertibleToWide(data)) {
|
||||
console.log('Converting response to the wide format');
|
||||
data = responseHandler.convertToWide(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { data };
|
||||
});
|
||||
return { data };
|
||||
});
|
||||
}
|
||||
|
||||
async dbConnectionQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> {
|
||||
const targets = request.targets.filter(this.isDBConnectionTarget);
|
||||
|
||||
const queries = _.compact(targets.map((target) => {
|
||||
// Don't request for hidden targets
|
||||
if (target.hide) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const queries = _.compact(
|
||||
targets.map((target) => {
|
||||
// Don't request for hidden targets
|
||||
if (target.hide) {
|
||||
return [];
|
||||
}
|
||||
return this.queryItemIdData(target, timeRange, useTrends, request);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
|
||||
// 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 this.queryItemIdData(target, timeRange, useTrends, request);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const promises: Promise<DataQueryResponse> = Promise.all(queries)
|
||||
.then(_.flatten)
|
||||
.then(data => ({ data }));
|
||||
.then(_.flatten)
|
||||
.then((data) => ({ data }));
|
||||
|
||||
return promises;
|
||||
}
|
||||
@@ -300,7 +322,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
*/
|
||||
async queryNumericData(target, timeRange, useTrends, request): Promise<any> {
|
||||
const getItemOptions = {
|
||||
itemtype: 'num'
|
||||
itemtype: 'num',
|
||||
};
|
||||
|
||||
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`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
hideFromInspector: false,
|
||||
data: {
|
||||
@@ -379,10 +401,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
getTrendValueType(target) {
|
||||
// Find trendValue() function and get specified trend value
|
||||
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 trendValueFunc ? trendValueFunc.params[0] : "avg";
|
||||
return trendValueFunc ? trendValueFunc.params[0] : 'avg';
|
||||
}
|
||||
|
||||
sortByRefId(response: DataQueryResponse) {
|
||||
@@ -413,18 +435,19 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
*/
|
||||
queryTextData(target, timeRange) {
|
||||
const options = {
|
||||
itemtype: 'text'
|
||||
itemtype: 'text',
|
||||
};
|
||||
return this.zabbix.getItemsFromTarget(target, options)
|
||||
.then(items => {
|
||||
return this.zabbix.getHistoryText(items, timeRange, target);
|
||||
})
|
||||
.then(result => {
|
||||
if (target.resultFormat !== 'table') {
|
||||
return result.map(s => responseHandler.seriesToDataFrame(s, target, [], FieldType.string));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
return this.zabbix
|
||||
.getItemsFromTarget(target, options)
|
||||
.then((items) => {
|
||||
return this.zabbix.getHistoryText(items, timeRange, target);
|
||||
})
|
||||
.then((result) => {
|
||||
if (target.resultFormat !== 'table') {
|
||||
return result.map((s) => responseHandler.seriesToDataFrame(s, target, [], FieldType.string));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -433,14 +456,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
queryItemIdData(target, timeRange, useTrends, options) {
|
||||
let itemids = target.itemids;
|
||||
itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
|
||||
itemids = _.map(itemids.split(','), itemid => itemid.trim());
|
||||
itemids = _.map(itemids.split(','), (itemid) => itemid.trim());
|
||||
|
||||
if (!itemids) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.zabbix.getItemsByIDs(itemids)
|
||||
.then(items => {
|
||||
return this.zabbix.getItemsByIDs(itemids).then((items) => {
|
||||
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);
|
||||
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 backendRequest = responseHandler.itServiceResponseToTimeSeries(itservicesdp, target.slaInterval);
|
||||
@@ -478,8 +500,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
|
||||
queryTriggersData(target, timeRange) {
|
||||
const [timeFrom, timeTo] = timeRange;
|
||||
return this.zabbix.getHostsFromTarget(target)
|
||||
.then(results => {
|
||||
return this.zabbix.getHostsFromTarget(target).then((results) => {
|
||||
const [hosts, apps] = results;
|
||||
if (hosts.length) {
|
||||
const hostids = _.map(hosts, 'hostid');
|
||||
@@ -489,14 +510,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
acknowledged: target.triggers.acknowledged,
|
||||
count: target.triggers.count,
|
||||
timeFrom: timeFrom,
|
||||
timeTo: timeTo
|
||||
timeTo: timeTo,
|
||||
};
|
||||
const groupFilter = target.group.filter;
|
||||
return Promise.all([
|
||||
this.zabbix.getHostAlerts(hostids, appids, options),
|
||||
this.zabbix.getGroups(groupFilter)
|
||||
])
|
||||
.then(([triggers, groups]) => {
|
||||
this.zabbix.getGroups(groupFilter),
|
||||
]).then(([triggers, groups]) => {
|
||||
return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
|
||||
});
|
||||
} else {
|
||||
@@ -536,7 +556,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
||||
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', '');
|
||||
const tags = utils.parseTags(tagsFilterStr);
|
||||
tags.forEach(tag => {
|
||||
tags.forEach((tag) => {
|
||||
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
|
||||
tag.operator = 1;
|
||||
});
|
||||
@@ -556,9 +576,9 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
}
|
||||
|
||||
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) {
|
||||
severities = severities.filter(v => target.options?.severities.includes(v));
|
||||
severities = severities.filter((v) => target.options?.severities.includes(v));
|
||||
}
|
||||
problemsOptions.severities = severities;
|
||||
}
|
||||
@@ -567,27 +587,30 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
if (showProblems === ShowProblemTypes.History || target.options?.useTimeRange) {
|
||||
problemsOptions.timeFrom = timeFrom;
|
||||
problemsOptions.timeTo = timeTo;
|
||||
getProblemsPromise = this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions);
|
||||
getProblemsPromise = this.zabbix.getProblemsHistory(
|
||||
groupFilter,
|
||||
hostFilter,
|
||||
appFilter,
|
||||
proxyFilter,
|
||||
problemsOptions
|
||||
);
|
||||
} else {
|
||||
getProblemsPromise = this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions);
|
||||
}
|
||||
|
||||
const problemsPromises = Promise.all([
|
||||
getProblemsPromise,
|
||||
getProxiesPromise
|
||||
])
|
||||
.then(([problems, sourceProxies]) => {
|
||||
proxies = _.keyBy(sourceProxies, 'proxyid');
|
||||
return problems;
|
||||
})
|
||||
.then(problems => problemsHandler.setMaintenanceStatus(problems))
|
||||
.then(problems => problemsHandler.setAckButtonStatus(problems, showAckButton))
|
||||
.then(problems => problemsHandler.filterTriggersPre(problems, replacedTarget))
|
||||
.then(problems => problemsHandler.sortProblems(problems, target))
|
||||
.then(problems => problemsHandler.addTriggerDataSource(problems, target))
|
||||
.then(problems => problemsHandler.addTriggerHostProxy(problems, proxies));
|
||||
const problemsPromises = Promise.all([getProblemsPromise, getProxiesPromise])
|
||||
.then(([problems, sourceProxies]) => {
|
||||
proxies = _.keyBy(sourceProxies, 'proxyid');
|
||||
return problems;
|
||||
})
|
||||
.then((problems) => problemsHandler.setMaintenanceStatus(problems))
|
||||
.then((problems) => problemsHandler.setAckButtonStatus(problems, showAckButton))
|
||||
.then((problems) => problemsHandler.filterTriggersPre(problems, replacedTarget))
|
||||
.then((problems) => problemsHandler.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);
|
||||
return problemsDataFrame;
|
||||
});
|
||||
@@ -604,35 +627,35 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
message += `, DB connector type: ${dbConnectorStatus.dsType}`;
|
||||
}
|
||||
return {
|
||||
status: "success",
|
||||
title: "Success",
|
||||
message: message
|
||||
status: 'success',
|
||||
title: 'Success',
|
||||
message: message,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ZabbixAPIError) {
|
||||
return {
|
||||
status: "error",
|
||||
status: 'error',
|
||||
title: error.message,
|
||||
message: error.message
|
||||
message: error.message,
|
||||
};
|
||||
} else if (error.data && error.data.message) {
|
||||
return {
|
||||
status: "error",
|
||||
title: "Zabbix Client Error",
|
||||
message: error.data.message
|
||||
status: 'error',
|
||||
title: 'Zabbix Client Error',
|
||||
message: error.data.message,
|
||||
};
|
||||
} else if (typeof (error) === 'string') {
|
||||
} else if (typeof error === 'string') {
|
||||
return {
|
||||
status: "error",
|
||||
title: "Unknown Error",
|
||||
message: error
|
||||
status: 'error',
|
||||
title: 'Unknown Error',
|
||||
message: error,
|
||||
};
|
||||
} else {
|
||||
console.log(error);
|
||||
return {
|
||||
status: "error",
|
||||
title: "Connection failed",
|
||||
message: "Could not connect to given url"
|
||||
status: 'error',
|
||||
title: 'Connection failed',
|
||||
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);
|
||||
break;
|
||||
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;
|
||||
case VariableQueryTypes.ItemValues:
|
||||
const range = options?.range;
|
||||
@@ -694,7 +723,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
break;
|
||||
}
|
||||
|
||||
return resultPromise.then(metrics => {
|
||||
return resultPromise.then((metrics) => {
|
||||
return _.map(metrics, formatMetric);
|
||||
});
|
||||
}
|
||||
@@ -718,74 +747,85 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
// 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 timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
|
||||
const annotation = options.annotation;
|
||||
const annotation = options.targets[0];
|
||||
|
||||
// Show all triggers
|
||||
const problemsOptions: any = {
|
||||
value: annotation.showOkEvents ? ['0', '1'] : '1',
|
||||
value: annotation.options.showOkEvents ? ['0', '1'] : '1',
|
||||
valueFromEvent: true,
|
||||
timeFrom,
|
||||
timeTo,
|
||||
};
|
||||
|
||||
if (annotation.minseverity) {
|
||||
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity));
|
||||
if (annotation.options.minSeverity) {
|
||||
const severities = [0, 1, 2, 3, 4, 5].filter((v) => v >= Number(annotation.options.minSeverity));
|
||||
problemsOptions.severities = severities;
|
||||
}
|
||||
|
||||
const groupFilter = this.replaceTemplateVars(annotation.group, {});
|
||||
const hostFilter = this.replaceTemplateVars(annotation.host, {});
|
||||
const appFilter = this.replaceTemplateVars(annotation.application, {});
|
||||
const groupFilter = this.replaceTemplateVars(annotation.group.filter, {});
|
||||
const hostFilter = this.replaceTemplateVars(annotation.host.filter, {});
|
||||
const appFilter = this.replaceTemplateVars(annotation.application.filter, {});
|
||||
const proxyFilter = undefined;
|
||||
|
||||
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
|
||||
.then(problems => {
|
||||
// Filter triggers by description
|
||||
const problemName = this.replaceTemplateVars(annotation.trigger, {});
|
||||
if (utils.isRegex(problemName)) {
|
||||
problems = _.filter(problems, p => {
|
||||
return utils.buildRegex(problemName).test(p.description);
|
||||
});
|
||||
} else if (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 this.zabbix
|
||||
.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
|
||||
.then((problems) => {
|
||||
// Filter triggers by description
|
||||
const problemName = this.replaceTemplateVars(annotation.trigger.filter, {});
|
||||
if (utils.isRegex(problemName)) {
|
||||
problems = _.filter(problems, (p) => {
|
||||
return utils.buildRegex(problemName).test(p.description);
|
||||
});
|
||||
} else if (problemName) {
|
||||
problems = _.filter(problems, (p) => {
|
||||
return p.description === problemName;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title: p.value === '1' ? 'Problem' : 'OK',
|
||||
time: p.timestamp * 1000,
|
||||
annotation: annotation,
|
||||
text: p.name + formattedAcknowledges,
|
||||
tags: annotationTags,
|
||||
};
|
||||
// Hide acknowledged events if option enabled
|
||||
if (annotation.hideAcknowledged) {
|
||||
problems = _.filter(problems, (p) => {
|
||||
return !p.acknowledges?.length;
|
||||
});
|
||||
}
|
||||
|
||||
return _.map(problems, (p) => {
|
||||
const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges);
|
||||
|
||||
let annotationTags: string[] = [];
|
||||
if (annotation.showHostname) {
|
||||
annotationTags = _.map(p.hosts, 'name');
|
||||
}
|
||||
|
||||
return {
|
||||
title: p.value === '1' ? 'Problem' : 'OK',
|
||||
time: p.timestamp * 1000,
|
||||
annotation: annotation,
|
||||
text: p.name + formattedAcknowledges,
|
||||
tags: annotationTags,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Replace template variables
|
||||
replaceTargetVariables(target, options) {
|
||||
const parts = ['group', 'host', 'application', 'itemTag', 'item'];
|
||||
_.forEach(parts, p => {
|
||||
_.forEach(parts, (p) => {
|
||||
if (target[p] && target[p].filter) {
|
||||
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);
|
||||
}
|
||||
|
||||
_.forEach(target.functions, func => {
|
||||
func.params = _.map(func.params, param => {
|
||||
_.forEach(target.functions, (func) => {
|
||||
func.params = _.map(func.params, (param) => {
|
||||
if (typeof param === 'number') {
|
||||
return +this.templateSrv.replace(param.toString(), options.scopedVars);
|
||||
} else {
|
||||
@@ -814,10 +854,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
const [timeFrom, timeTo] = timeRange;
|
||||
const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
|
||||
const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
|
||||
const useTrends = this.trends && (
|
||||
(timeFrom < useTrendsFrom) ||
|
||||
(timeTo - timeFrom > useTrendsRange)
|
||||
);
|
||||
const useTrends = this.trends && (timeFrom < useTrendsFrom || timeTo - timeFrom > useTrendsRange);
|
||||
return useTrends;
|
||||
}
|
||||
|
||||
@@ -826,23 +863,21 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return false;
|
||||
}
|
||||
|
||||
return target.queryType === c.MODE_METRICS ||
|
||||
target.queryType === c.MODE_ITEMID;
|
||||
return target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID;
|
||||
};
|
||||
|
||||
isDBConnectionTarget = (target: any): boolean => {
|
||||
return this.enableDirectDBConnection &&
|
||||
(target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID);
|
||||
return this.enableDirectDBConnection && (target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID);
|
||||
};
|
||||
}
|
||||
|
||||
function bindFunctionDefs(functionDefs, category) {
|
||||
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
|
||||
const aggFuncDefs = _.filter(functionDefs, func => {
|
||||
const aggFuncDefs = _.filter(functionDefs, (func) => {
|
||||
return _.includes(aggregationFunctions, func.def.name);
|
||||
});
|
||||
|
||||
return _.map(aggFuncDefs, func => {
|
||||
return _.map(aggFuncDefs, (func) => {
|
||||
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
|
||||
return funcInstance.bindFunction(dataProcessor.metricFunctions);
|
||||
});
|
||||
@@ -850,7 +885,7 @@ function bindFunctionDefs(functionDefs, category) {
|
||||
|
||||
function getConsolidateBy(target) {
|
||||
let consolidateBy;
|
||||
const funcDef = _.find(target.functions, func => {
|
||||
const funcDef = _.find(target.functions, (func) => {
|
||||
return func.def.name === 'consolidateBy';
|
||||
});
|
||||
if (funcDef && funcDef.params && funcDef.params.length) {
|
||||
@@ -862,7 +897,7 @@ function getConsolidateBy(target) {
|
||||
function formatMetric(metricObj) {
|
||||
return {
|
||||
text: metricObj.name,
|
||||
expandable: false
|
||||
expandable: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) : ' ');
|
||||
}
|
||||
|
||||
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' : '') + '">, </span>').appendTo(elem);
|
||||
}
|
||||
|
||||
const $paramLink = $(
|
||||
'<a ng-click="" class="graphite-func-param-link' +
|
||||
(last ? ' query-part__last' : '') +
|
||||
'">' +
|
||||
(hasValue ? paramValue : ' ') +
|
||||
'</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']);
|
||||
@@ -1,18 +1,19 @@
|
||||
import _ from 'lodash';
|
||||
import { FuncDef } from './types';
|
||||
import { isNumeric } from './utils';
|
||||
|
||||
const index = [];
|
||||
const categories = {
|
||||
const index = {};
|
||||
const categories: { [key: string]: FuncDef[] } = {
|
||||
Transform: [],
|
||||
Aggregate: [],
|
||||
Filter: [],
|
||||
Trends: [],
|
||||
Time: [],
|
||||
Alias: [],
|
||||
Special: []
|
||||
Special: [],
|
||||
};
|
||||
|
||||
function addFuncDef(funcDef) {
|
||||
function addFuncDef(funcDef: FuncDef) {
|
||||
funcDef.params = funcDef.params || [];
|
||||
funcDef.defaultParams = funcDef.defaultParams || [];
|
||||
|
||||
@@ -29,8 +30,8 @@ addFuncDef({
|
||||
name: 'groupBy',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{ name: 'interval', type: 'string'},
|
||||
{ name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] }
|
||||
{ name: 'interval', type: 'string' },
|
||||
{ name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median', 'first', 'last'] },
|
||||
],
|
||||
defaultParams: ['1m', 'avg'],
|
||||
});
|
||||
@@ -38,18 +39,14 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'scale',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{ name: 'factor', type: 'float', options: [100, 0.01, 10, -1]}
|
||||
],
|
||||
params: [{ name: 'factor', type: 'float', options: [100, 0.01, 10, -1] }],
|
||||
defaultParams: [100],
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'offset',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{ name: 'delta', type: 'float', options: [-100, 100]}
|
||||
],
|
||||
params: [{ name: 'delta', type: 'float', options: [-100, 100] }],
|
||||
defaultParams: [100],
|
||||
});
|
||||
|
||||
@@ -70,18 +67,14 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'movingAverage',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{ name: 'factor', type: 'int', options: [6, 10, 60, 100, 600] }
|
||||
],
|
||||
params: [{ name: 'factor', type: 'int', options: [6, 10, 60, 100, 600] }],
|
||||
defaultParams: [10],
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'exponentialMovingAverage',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{ name: 'smoothing', type: 'float', options: [6, 10, 60, 100, 600] }
|
||||
],
|
||||
params: [{ name: 'smoothing', type: 'float', options: [6, 10, 60, 100, 600] }],
|
||||
defaultParams: [0.2],
|
||||
});
|
||||
|
||||
@@ -90,7 +83,7 @@ addFuncDef({
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{ 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],
|
||||
});
|
||||
@@ -98,27 +91,21 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'removeAboveValue',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{name: 'number', type: 'float'},
|
||||
],
|
||||
params: [{ name: 'number', type: 'float' }],
|
||||
defaultParams: [0],
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'removeBelowValue',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{name: 'number', type: 'float'},
|
||||
],
|
||||
params: [{ name: 'number', type: 'float' }],
|
||||
defaultParams: [0],
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'transformNull',
|
||||
category: 'Transform',
|
||||
params: [
|
||||
{name: 'number', type: 'float'}
|
||||
],
|
||||
params: [{ name: 'number', type: 'float' }],
|
||||
defaultParams: [0],
|
||||
});
|
||||
|
||||
@@ -129,7 +116,7 @@ addFuncDef({
|
||||
category: 'Aggregate',
|
||||
params: [
|
||||
{ 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'],
|
||||
});
|
||||
@@ -146,7 +133,7 @@ addFuncDef({
|
||||
category: 'Aggregate',
|
||||
params: [
|
||||
{ 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],
|
||||
});
|
||||
@@ -158,7 +145,7 @@ addFuncDef({
|
||||
category: 'Filter',
|
||||
params: [
|
||||
{ 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'],
|
||||
});
|
||||
@@ -168,7 +155,7 @@ addFuncDef({
|
||||
category: 'Filter',
|
||||
params: [
|
||||
{ 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'],
|
||||
});
|
||||
@@ -176,10 +163,8 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'sortSeries',
|
||||
category: 'Filter',
|
||||
params: [
|
||||
{ name: 'direction', type: 'string', options: ['asc', 'desc'] }
|
||||
],
|
||||
defaultParams: ['asc']
|
||||
params: [{ name: 'direction', type: 'string', options: ['asc', 'desc'] }],
|
||||
defaultParams: ['asc'],
|
||||
});
|
||||
|
||||
// Trends
|
||||
@@ -187,9 +172,7 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'trendValue',
|
||||
category: 'Trends',
|
||||
params: [
|
||||
{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }
|
||||
],
|
||||
params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
|
||||
defaultParams: ['avg'],
|
||||
});
|
||||
|
||||
@@ -198,9 +181,7 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'timeShift',
|
||||
category: 'Time',
|
||||
params: [
|
||||
{ name: 'interval', type: 'string', options: ['24h', '7d', '1M', '+24h', '-24h']}
|
||||
],
|
||||
params: [{ name: 'interval', type: 'string', options: ['24h', '7d', '1M', '+24h', '-24h'] }],
|
||||
defaultParams: ['24h'],
|
||||
});
|
||||
|
||||
@@ -209,19 +190,15 @@ addFuncDef({
|
||||
addFuncDef({
|
||||
name: 'setAlias',
|
||||
category: 'Alias',
|
||||
params: [
|
||||
{ name: 'alias', type: 'string' }
|
||||
],
|
||||
defaultParams: []
|
||||
params: [{ name: 'alias', type: 'string' }],
|
||||
defaultParams: [],
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
name: 'setAliasByRegex',
|
||||
category: 'Alias',
|
||||
params: [
|
||||
{ name: 'aliasByRegex', type: 'string' }
|
||||
],
|
||||
defaultParams: []
|
||||
params: [{ name: 'aliasByRegex', type: 'string' }],
|
||||
defaultParams: [],
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@@ -229,18 +206,16 @@ addFuncDef({
|
||||
category: 'Alias',
|
||||
params: [
|
||||
{ name: 'regexp', type: 'string' },
|
||||
{ name: 'newAlias', type: 'string' }
|
||||
{ name: 'newAlias', type: 'string' },
|
||||
],
|
||||
defaultParams: ['/(.*)/', '$1']
|
||||
defaultParams: ['/(.*)/', '$1'],
|
||||
});
|
||||
|
||||
// Special
|
||||
addFuncDef({
|
||||
name: 'consolidateBy',
|
||||
category: 'Special',
|
||||
params: [
|
||||
{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }
|
||||
],
|
||||
params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
|
||||
defaultParams: ['avg'],
|
||||
});
|
||||
|
||||
@@ -271,7 +246,6 @@ class FuncInstance {
|
||||
bindFunction(metricFunctions) {
|
||||
const func = metricFunctions[this.def.name];
|
||||
if (func) {
|
||||
|
||||
// Bind function arguments
|
||||
let bindedFunc = func;
|
||||
let param;
|
||||
@@ -279,8 +253,7 @@ class FuncInstance {
|
||||
param = this.params[i];
|
||||
|
||||
// Convert numeric params
|
||||
if (this.def.params[i].type === 'int' ||
|
||||
this.def.params[i].type === 'float') {
|
||||
if (this.def.params[i].type === 'int' || this.def.params[i].type === 'float') {
|
||||
param = Number(param);
|
||||
}
|
||||
bindedFunc = _.partial(bindedFunc, param);
|
||||
@@ -295,17 +268,13 @@ class FuncInstance {
|
||||
const str = this.def.name + '(';
|
||||
const parameters = _.map(this.params, (value, index) => {
|
||||
const paramType = this.def.params[index].type;
|
||||
if (paramType === 'int' ||
|
||||
paramType === 'float' ||
|
||||
paramType === 'value_or_series' ||
|
||||
paramType === 'boolean') {
|
||||
if (paramType === 'int' || paramType === 'float' || paramType === 'value_or_series' || paramType === 'boolean') {
|
||||
return value;
|
||||
} else if (paramType === 'int_or_interval' && isNumeric(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "'" + value + "'";
|
||||
|
||||
});
|
||||
|
||||
if (metricExp) {
|
||||
@@ -335,7 +304,7 @@ class FuncInstance {
|
||||
|
||||
if (strValue === '' && this.def.params[index].optional) {
|
||||
this.params.splice(index, 1);
|
||||
}else {
|
||||
} else {
|
||||
this.params[index] = strValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import * as c from './constants';
|
||||
|
||||
export function isGrafana2target(target) {
|
||||
if (!target.mode || target.mode === 0 || target.mode === 2) {
|
||||
if ((target.hostFilter || target.itemFilter || target.downsampleFunction ||
|
||||
(target.host && target.host.host)) &&
|
||||
(target.item.filter === undefined && target.host.filter === undefined)) {
|
||||
if (
|
||||
(target.hostFilter || target.itemFilter || target.downsampleFunction || (target.host && target.host.host)) &&
|
||||
target.item.filter === undefined &&
|
||||
target.host.filter === undefined
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -22,10 +24,10 @@ export function isGrafana2target(target) {
|
||||
}
|
||||
|
||||
export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
|
||||
target.group.filter = target.group.name === "*" ? "/.*/" : target.group.name;
|
||||
target.host.filter = target.host.name === "*" ? convertToRegex(target.hostFilter) : target.host.name;
|
||||
target.application.filter = target.application.name === "*" ? "" : target.application.name;
|
||||
target.item.filter = target.item.name === "All" ? convertToRegex(target.itemFilter) : target.item.name;
|
||||
target.group.filter = target.group.name === '*' ? '/.*/' : target.group.name;
|
||||
target.host.filter = target.host.name === '*' ? convertToRegex(target.hostFilter) : target.host.name;
|
||||
target.application.filter = target.application.name === '*' ? '' : target.application.name;
|
||||
target.item.filter = target.item.name === 'All' ? convertToRegex(target.itemFilter) : target.item.name;
|
||||
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) {
|
||||
target.resultFormat = target.resultFormat || 'time_series';
|
||||
target = fixTargetGroup(target);
|
||||
@@ -88,12 +96,13 @@ export function migrate(target) {
|
||||
migrateSLA(target);
|
||||
migrateProblemSort(target);
|
||||
migrateApplications(target);
|
||||
migrateSLAProperty(target);
|
||||
return target;
|
||||
}
|
||||
|
||||
function fixTargetGroup(target) {
|
||||
if (target.group && Array.isArray(target.group)) {
|
||||
target.group = { 'filter': "" };
|
||||
target.group = { filter: '' };
|
||||
}
|
||||
return target;
|
||||
}
|
||||
@@ -128,7 +137,7 @@ export function migrateDSConfig(jsonData) {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -143,3 +152,34 @@ function shouldMigrateDSConfig(jsonData): boolean {
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { loadPluginCss } from '@grafana/runtime';
|
||||
import { ZabbixDatasource } from './datasource';
|
||||
import { ZabbixQueryController } from './query.controller';
|
||||
import { QueryEditor } from './components/QueryEditor';
|
||||
import { ZabbixVariableQueryEditor } from './components/VariableQueryEditor';
|
||||
import { ConfigEditor } from './components/ConfigEditor';
|
||||
import './add-metric-function.directive';
|
||||
import './metric-function-editor.directive';
|
||||
|
||||
class ZabbixAnnotationsQueryController {
|
||||
static templateUrl = 'datasource-zabbix/partials/annotations.editor.html';
|
||||
}
|
||||
|
||||
ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html';
|
||||
import '../sass/grafana-zabbix.dark.scss';
|
||||
import '../sass/grafana-zabbix.light.scss';
|
||||
|
||||
loadPluginCss({
|
||||
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)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setQueryCtrl(ZabbixQueryController)
|
||||
.setAnnotationQueryCtrl(ZabbixAnnotationsQueryController)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setVariableQueryEditor(ZabbixVariableQueryEditor);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -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 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) => {
|
||||
const item = _.find(items, { 'itemid': itemid }) as any;
|
||||
const item = _.find(items, { itemid: itemid }) as any;
|
||||
let alias = item.name;
|
||||
|
||||
// Add scopedVars for using in alias functions
|
||||
const scopedVars: any = {
|
||||
'__zbx_item': { value: item.name },
|
||||
'__zbx_item_name': { value: item.name },
|
||||
'__zbx_item_key': { value: item.key_ },
|
||||
'__zbx_item_interval': { value: item.delay },
|
||||
__zbx_item: { value: item.name },
|
||||
__zbx_item_name: { value: item.name },
|
||||
__zbx_item_key: { value: item.key_ },
|
||||
__zbx_item_interval: { value: item.delay },
|
||||
};
|
||||
|
||||
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_name'] = { value: host.name };
|
||||
|
||||
// Only add host when multiple hosts selected
|
||||
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,
|
||||
datapoints: _.map(hist, convertPointCallback),
|
||||
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 timeFiled: Field = {
|
||||
name: TIME_SERIES_TIME_FIELD_NAME,
|
||||
type: FieldType.time,
|
||||
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>;
|
||||
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 {
|
||||
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 = {
|
||||
@@ -99,7 +104,7 @@ export function seriesToDataFrame(timeseries, target: ZabbixMetricsQuery, valueM
|
||||
labels: {},
|
||||
config: {
|
||||
displayNameFromDS: seriesName,
|
||||
custom: {}
|
||||
custom: {},
|
||||
},
|
||||
values,
|
||||
};
|
||||
@@ -179,7 +184,7 @@ export function dataResponseToTimeSeries(response: DataFrameJSON[], items, reque
|
||||
}
|
||||
|
||||
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
|
||||
let interval = request.intervalMs * 1000000;
|
||||
@@ -201,7 +206,7 @@ export function dataResponseToTimeSeries(response: DataFrameJSON[], items, reque
|
||||
name: seriesName,
|
||||
item,
|
||||
interval,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
series.push(timeSeriesData);
|
||||
@@ -263,7 +268,7 @@ export function itServiceResponseToTimeSeries(response: any, interval) {
|
||||
name: s.target,
|
||||
interval: null,
|
||||
item: {},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
series.push(timeSeriesData);
|
||||
@@ -277,13 +282,13 @@ export function isConvertibleToWide(data: DataFrame[]): boolean {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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++) {
|
||||
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
|
||||
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++) {
|
||||
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);
|
||||
if (firstTs < minTimestamp) {
|
||||
minTimestamp = firstTs;
|
||||
@@ -312,8 +317,8 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const frame = data[i];
|
||||
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 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 firstTs = timeField.values.get(0);
|
||||
|
||||
if (firstTs > minTimestamp) {
|
||||
@@ -340,7 +345,7 @@ export function alignFrames(data: MutableDataFrame[]): MutableDataFrame[] {
|
||||
|
||||
export function convertToWide(data: MutableDataFrame[]): DataFrame[] {
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
@@ -348,7 +353,7 @@ export function convertToWide(data: MutableDataFrame[]): DataFrame[] {
|
||||
const fields: MutableField[] = [timeField];
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
@@ -363,7 +368,7 @@ export function convertToWide(data: MutableDataFrame[]): DataFrame[] {
|
||||
}
|
||||
|
||||
const frame: DataFrame = {
|
||||
name: "wide",
|
||||
name: 'wide',
|
||||
fields,
|
||||
length: timeField.values.length,
|
||||
};
|
||||
@@ -375,7 +380,7 @@ function getLongestFrame(data: MutableDataFrame[]): number {
|
||||
let maxLengthIndex = 0;
|
||||
let maxLength = 0;
|
||||
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) {
|
||||
maxLength = timeField.values.length;
|
||||
maxLengthIndex = i;
|
||||
@@ -387,8 +392,8 @@ function getLongestFrame(data: MutableDataFrame[]): number {
|
||||
|
||||
function sortTimeseries(timeseries) {
|
||||
// Sort trend data, issue #202
|
||||
_.forEach(timeseries, series => {
|
||||
series.datapoints = _.sortBy(series.datapoints, point => point[c.DATAPOINT_TS]);
|
||||
_.forEach(timeseries, (series) => {
|
||||
series.datapoints = _.sortBy(series.datapoints, (point) => point[c.DATAPOINT_TS]);
|
||||
});
|
||||
return timeseries;
|
||||
}
|
||||
@@ -430,11 +435,9 @@ function handleHistoryAsTable(history, items, target) {
|
||||
}
|
||||
|
||||
let host: any = _.first(item.hosts);
|
||||
host = host ? host.name : "";
|
||||
host = host ? host.name : '';
|
||||
|
||||
table.rows.push([
|
||||
host, item.name, item.key_, lastValue
|
||||
]);
|
||||
table.rows.push([host, item.name, item.key_, lastValue]);
|
||||
});
|
||||
|
||||
return table;
|
||||
@@ -448,10 +451,7 @@ function convertText(target, point) {
|
||||
value = extractText(point.value, target.textFilter, target.useCaptureGroups);
|
||||
}
|
||||
|
||||
return [
|
||||
value,
|
||||
point.clock * 1000 + Math.round(point.ns / 1000000)
|
||||
];
|
||||
return [value, point.clock * 1000 + Math.round(point.ns / 1000000)];
|
||||
}
|
||||
|
||||
function extractText(str, pattern, useCaptureGroups) {
|
||||
@@ -464,31 +464,29 @@ function extractText(str, pattern, useCaptureGroups) {
|
||||
return extractedValue[0];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
function handleSLAResponse(itservice, slaProperty, slaObject) {
|
||||
const targetSLA = slaObject[itservice.serviceid].sla;
|
||||
if (slaProperty.property === 'status') {
|
||||
if (slaProperty === 'status') {
|
||||
const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10);
|
||||
return {
|
||||
target: itservice.name + ' ' + slaProperty.name,
|
||||
datapoints: [
|
||||
[targetStatus, targetSLA[0].to * 1000]
|
||||
]
|
||||
target: itservice.name + ' ' + slaProperty,
|
||||
datapoints: [[targetStatus, targetSLA[0].to * 1000]],
|
||||
};
|
||||
} else {
|
||||
let i;
|
||||
const slaArr = [];
|
||||
for (i = 0; i < targetSLA.length; i++) {
|
||||
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 {
|
||||
target: itservice.name + ' ' + slaProperty.name,
|
||||
datapoints: slaArr
|
||||
target: itservice.name + ' ' + slaProperty,
|
||||
datapoints: slaArr,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -499,13 +497,11 @@ function handleTriggersResponse(triggers, groups, timeRange) {
|
||||
try {
|
||||
triggersCount = Number(triggers);
|
||||
} catch (err) {
|
||||
console.log("Error when handling triggers count: ", err);
|
||||
console.log('Error when handling triggers count: ', err);
|
||||
}
|
||||
return {
|
||||
target: "triggers count",
|
||||
datapoints: [
|
||||
[triggersCount, timeRange[1] * 1000]
|
||||
]
|
||||
target: 'triggers count',
|
||||
datapoints: [[triggersCount, timeRange[1] * 1000]],
|
||||
};
|
||||
} else {
|
||||
const stats = getTriggerStats(triggers);
|
||||
@@ -517,7 +513,10 @@ function handleTriggersResponse(triggers, groups, timeRange) {
|
||||
});
|
||||
_.each(stats, (severity_stats, 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);
|
||||
table.rows.push(row);
|
||||
}
|
||||
@@ -543,38 +542,32 @@ function getTriggerStats(triggers) {
|
||||
|
||||
function convertHistoryPoint(point) {
|
||||
// Value must be a number for properly work
|
||||
return [
|
||||
Number(point.value),
|
||||
point.clock * 1000 + Math.round(point.ns / 1000000)
|
||||
];
|
||||
return [Number(point.value), point.clock * 1000 + Math.round(point.ns / 1000000)];
|
||||
}
|
||||
|
||||
function convertTrendPoint(valueType, point) {
|
||||
let value;
|
||||
switch (valueType) {
|
||||
case "min":
|
||||
case 'min':
|
||||
value = point.value_min;
|
||||
break;
|
||||
case "max":
|
||||
case 'max':
|
||||
value = point.value_max;
|
||||
break;
|
||||
case "avg":
|
||||
case 'avg':
|
||||
value = point.value_avg;
|
||||
break;
|
||||
case "sum":
|
||||
case 'sum':
|
||||
value = point.value_avg * point.num;
|
||||
break;
|
||||
case "count":
|
||||
case 'count':
|
||||
value = point.num;
|
||||
break;
|
||||
default:
|
||||
value = point.value_avg;
|
||||
}
|
||||
|
||||
return [
|
||||
Number(value),
|
||||
point.clock * 1000
|
||||
];
|
||||
return [Number(value), point.clock * 1000];
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -13,6 +13,10 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
}), { virtual: true });
|
||||
|
||||
jest.mock('../components/AnnotationQueryEditor', () => ({
|
||||
AnnotationQueryEditor: () => {},
|
||||
}));
|
||||
|
||||
describe('ZabbixDatasource', () => {
|
||||
let ctx = {};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
username: string;
|
||||
@@ -34,24 +34,26 @@ export interface ZabbixConnectionTestQuery {
|
||||
}
|
||||
|
||||
export interface ZabbixMetricsQuery extends DataQuery {
|
||||
triggers: { minSeverity: string; acknowledged: boolean; count: number; };
|
||||
queryType: string;
|
||||
datasourceId: number;
|
||||
group: { filter: string; name?: string; };
|
||||
host: { filter: string; name?: string; };
|
||||
application: { filter: string; name?: string; };
|
||||
itemTag: { filter: string; name?: string; };
|
||||
item: { filter: string; name?: string; };
|
||||
textFilter: string;
|
||||
mode: number;
|
||||
itemids: number[];
|
||||
useCaptureGroups: boolean;
|
||||
proxy?: { filter: string; };
|
||||
trigger?: { filter: string; };
|
||||
datasourceId?: number;
|
||||
group?: { filter: string; name?: string };
|
||||
host?: { filter: string; name?: string };
|
||||
application?: { filter: string; name?: string };
|
||||
itemTag?: { filter: string; name?: string };
|
||||
item?: { filter: string; name?: string };
|
||||
textFilter?: string;
|
||||
mode?: number;
|
||||
itemids?: string;
|
||||
useCaptureGroups?: boolean;
|
||||
proxy?: { filter: string };
|
||||
trigger?: { filter: string };
|
||||
itServiceFilter?: string;
|
||||
tags?: { filter: string; };
|
||||
functions: ZabbixMetricFunction[];
|
||||
options: ZabbixQueryOptions;
|
||||
slaProperty?: any;
|
||||
slaInterval?: string;
|
||||
tags?: { filter: string };
|
||||
triggers?: { minSeverity: number; acknowledged: number; count: boolean };
|
||||
functions?: MetricFunc[];
|
||||
options?: ZabbixQueryOptions;
|
||||
// Problems
|
||||
showProblems?: ShowProblemTypes;
|
||||
// Deprecated
|
||||
@@ -73,14 +75,44 @@ export interface ZabbixQueryOptions {
|
||||
limit?: number;
|
||||
useTimeRange?: boolean;
|
||||
severities?: number[];
|
||||
|
||||
// Annotations
|
||||
showOkEvents?: boolean;
|
||||
hideAcknowledged?: boolean;
|
||||
showHostname?: boolean;
|
||||
}
|
||||
|
||||
export interface ZabbixMetricFunction {
|
||||
name: string;
|
||||
params: any;
|
||||
def: { name: string; params: any; };
|
||||
export interface MetricFunc {
|
||||
text: string;
|
||||
params: Array<string | number>;
|
||||
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
|
||||
// either. Safer not to bother trying to import them just for type hinting.
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\
|
||||
* @return {string} expanded name, ie "CPU system time"
|
||||
*/
|
||||
export function expandItemName(name: string, key: string): string {
|
||||
|
||||
// extract params from key:
|
||||
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
|
||||
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) {
|
||||
_.forEach(items, item => {
|
||||
_.forEach(items, (item) => {
|
||||
item.item = item.name;
|
||||
item.name = expandItemName(item.item, item.key_);
|
||||
return item;
|
||||
@@ -49,7 +48,7 @@ function splitKeyParams(paramStr) {
|
||||
const split_symbol = ',';
|
||||
let param = '';
|
||||
|
||||
_.forEach(paramStr, symbol => {
|
||||
_.forEach(paramStr, (symbol) => {
|
||||
if (symbol === '"' && in_array) {
|
||||
param += symbol;
|
||||
} else if (symbol === '"' && quoted) {
|
||||
@@ -81,14 +80,14 @@ export function containsMacro(itemName) {
|
||||
export function replaceMacro(item, macros, isTriggerItem?) {
|
||||
let itemName = isTriggerItem ? item.url : item.name;
|
||||
const item_macros = itemName.match(MACRO_PATTERN);
|
||||
_.forEach(item_macros, macro => {
|
||||
const host_macros = _.filter(macros, m => {
|
||||
_.forEach(item_macros, (macro) => {
|
||||
const host_macros = _.filter(macros, (m) => {
|
||||
if (m.hostid) {
|
||||
if (isTriggerItem) {
|
||||
// Trigger item can have multiple hosts
|
||||
// Check all trigger host ids against macro host id
|
||||
let hostIdFound = false;
|
||||
_.forEach(item.hosts, h => {
|
||||
_.forEach(item.hosts, (h) => {
|
||||
if (h.hostid === m.hostid) {
|
||||
hostIdFound = true;
|
||||
}
|
||||
@@ -116,7 +115,7 @@ export function replaceMacro(item, macros, isTriggerItem?) {
|
||||
}
|
||||
|
||||
function escapeMacro(macro) {
|
||||
macro = macro.replace(/\$/, '\\\$');
|
||||
macro = macro.replace(/\$/, '\\$');
|
||||
return macro;
|
||||
}
|
||||
|
||||
@@ -125,7 +124,7 @@ export function parseLegacyVariableQuery(query: string): VariableQuery {
|
||||
const parts = [];
|
||||
|
||||
// Split query. Query structure: group.host.app.item
|
||||
_.each(splitTemplateQuery(query), part => {
|
||||
_.each(splitTemplateQuery(query), (part) => {
|
||||
// Replace wildcard to regex
|
||||
if (part === '*') {
|
||||
part = '/.*/';
|
||||
@@ -176,7 +175,7 @@ export function splitTemplateQuery(query) {
|
||||
|
||||
if (isContainsBraces(query)) {
|
||||
const result = query.match(splitPattern);
|
||||
split = _.map(result, part => {
|
||||
split = _.map(result, (part) => {
|
||||
return _.trim(part, '{}');
|
||||
});
|
||||
} else {
|
||||
@@ -201,7 +200,7 @@ export function isRegex(str) {
|
||||
export function isTemplateVariable(str, templateVariables) {
|
||||
const variablePattern = /^\$\w+/;
|
||||
if (variablePattern.test(str)) {
|
||||
const variables = _.map(templateVariables, variable => {
|
||||
const variables = _.map(templateVariables, (variable) => {
|
||||
return '$' + variable.name;
|
||||
});
|
||||
return _.includes(variables, str);
|
||||
@@ -225,7 +224,7 @@ export function getRangeScopedVars(range) {
|
||||
export function buildRegex(str) {
|
||||
const matches = str.match(regexPattern);
|
||||
const pattern = matches[1];
|
||||
const flags = matches[2] !== "" ? matches[2] : undefined;
|
||||
const flags = matches[2] !== '' ? matches[2] : undefined;
|
||||
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 momentInterval: any[] = intervalPattern.exec(interval);
|
||||
const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]);
|
||||
return (duration.valueOf() as number);
|
||||
return duration.valueOf() as number;
|
||||
}
|
||||
|
||||
export function parseTimeShiftInterval(interval) {
|
||||
@@ -285,15 +284,30 @@ export function parseTimeShiftInterval(interval) {
|
||||
*/
|
||||
export function formatAcknowledges(acknowledges) {
|
||||
if (acknowledges.length) {
|
||||
let formatted_acknowledges = '<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 => {
|
||||
const timestamp = moment.unix(ack.clock);
|
||||
return '<tr><td><i>' + 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);
|
||||
});
|
||||
let formatted_acknowledges =
|
||||
'<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) => {
|
||||
const timestamp = moment.unix(ack.clock);
|
||||
return (
|
||||
'<tr><td><i>' +
|
||||
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>');
|
||||
return formatted_acknowledges;
|
||||
} else {
|
||||
@@ -307,7 +321,7 @@ export function convertToZabbixAPIUrl(url) {
|
||||
if (url.match(zabbixAPIUrlPattern)) {
|
||||
return url;
|
||||
} else {
|
||||
return url.replace(trimSlashPattern, "$1");
|
||||
return url.replace(trimSlashPattern, '$1');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,14 +333,16 @@ export function callOnce(func, promiseKeeper) {
|
||||
return function () {
|
||||
if (!promiseKeeper) {
|
||||
promiseKeeper = Promise.resolve(
|
||||
func.apply(this, arguments)
|
||||
.then(result => {
|
||||
promiseKeeper = null;
|
||||
return result;
|
||||
}).catch(err => {
|
||||
promiseKeeper = null;
|
||||
throw err;
|
||||
})
|
||||
func
|
||||
.apply(this, arguments)
|
||||
.then((result) => {
|
||||
promiseKeeper = null;
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
promiseKeeper = null;
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
return promiseKeeper;
|
||||
@@ -426,19 +442,19 @@ export function mustArray(result: any): any[] {
|
||||
|
||||
const getUnitsMap = () => ({
|
||||
'%': 'percent',
|
||||
'b': 'decbits', // bits(SI)
|
||||
'bps': 'bps', // bits/sec(SI)
|
||||
'B': 'bytes', // bytes(IEC)
|
||||
'Bps': 'binBps', // bytes/sec(IEC)
|
||||
b: 'decbits', // bits(SI)
|
||||
bps: 'bps', // bits/sec(SI)
|
||||
B: 'bytes', // bytes(IEC)
|
||||
Bps: 'binBps', // bytes/sec(IEC)
|
||||
// 'unixtime': 'dateTimeAsSystem',
|
||||
'uptime': 'dtdhms',
|
||||
'qps': 'qps', // requests/sec (rps)
|
||||
'iops': 'iops', // I/O ops/sec (iops)
|
||||
'Hz': 'hertz', // Hertz (1/s)
|
||||
'V': 'volt', // Volt (V)
|
||||
'C': 'celsius', // Celsius (°C)
|
||||
'RPM': 'rotrpm', // Revolutions per minute (rpm)
|
||||
'dBm': 'dBm', // Decibel-milliwatt (dBm)
|
||||
uptime: 'dtdhms',
|
||||
qps: 'qps', // requests/sec (rps)
|
||||
iops: 'iops', // I/O ops/sec (iops)
|
||||
Hz: 'hertz', // Hertz (1/s)
|
||||
V: 'volt', // Volt (V)
|
||||
C: 'celsius', // Celsius (°C)
|
||||
RPM: 'rotrpm', // Revolutions per minute (rpm)
|
||||
dBm: 'dBm', // Decibel-milliwatt (dBm)
|
||||
});
|
||||
|
||||
const getKnownGrafanaUnits = () => {
|
||||
@@ -466,7 +482,7 @@ export function convertZabbixUnit(zabbixUnit: string): string {
|
||||
|
||||
export function getValueMapping(item, valueMappings: any[]): ValueMapping[] | null {
|
||||
const { valuemapid } = item;
|
||||
const mapping = valueMappings?.find(m => m.valuemapid === valuemapid);
|
||||
const mapping = valueMappings?.find((m) => m.valuemapid === valuemapid);
|
||||
if (!mapping) {
|
||||
return null;
|
||||
}
|
||||
@@ -478,12 +494,33 @@ export function getValueMapping(item, valueMappings: any[]): ValueMapping[] | nu
|
||||
options: {
|
||||
value: m.value,
|
||||
text: m.newvalue,
|
||||
}
|
||||
},
|
||||
};
|
||||
return valueMapping;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
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 { Button, VerticalGroup, Spinner, Modal, Checkbox, RadioButtonGroup, stylesFactory, withTheme, Themeable, TextArea } from '@grafana/ui';
|
||||
import {
|
||||
ZBX_ACK_ACTION_ADD_MESSAGE,
|
||||
ZBX_ACK_ACTION_ACK,
|
||||
ZBX_ACK_ACTION_CHANGE_SEVERITY,
|
||||
ZBX_ACK_ACTION_CLOSE,
|
||||
} from '../../datasource-zabbix/constants';
|
||||
import {
|
||||
Button,
|
||||
VerticalGroup,
|
||||
Spinner,
|
||||
Modal,
|
||||
Checkbox,
|
||||
RadioButtonGroup,
|
||||
stylesFactory,
|
||||
withTheme,
|
||||
Themeable,
|
||||
TextArea,
|
||||
} from '@grafana/ui';
|
||||
import { FAIcon } from '../../components';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
@@ -37,12 +53,12 @@ export interface AckProblemData {
|
||||
}
|
||||
|
||||
const severityOptions = [
|
||||
{value: 0, label: 'Not classified'},
|
||||
{value: 1, label: 'Information'},
|
||||
{value: 2, label: 'Warning'},
|
||||
{value: 3, label: 'Average'},
|
||||
{value: 4, label: 'High'},
|
||||
{value: 5, label: 'Disaster'}
|
||||
{ value: 0, label: 'Not classified' },
|
||||
{ value: 1, label: 'Information' },
|
||||
{ value: 2, label: 'Warning' },
|
||||
{ value: 3, label: 'Average' },
|
||||
{ value: 4, label: 'High' },
|
||||
{ value: 5, label: 'Disaster' },
|
||||
];
|
||||
|
||||
export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||
@@ -67,7 +83,7 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||
|
||||
handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({ value: event.target.value, error: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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') {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleBackdropClick = () => {
|
||||
this.dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
onAcknowledgeToggle = () => {
|
||||
this.setState({ acknowledge: !this.state.acknowledge, error: false });
|
||||
}
|
||||
};
|
||||
|
||||
onChangeSeverityToggle = () => {
|
||||
this.setState({ changeSeverity: !this.state.changeSeverity, error: false });
|
||||
}
|
||||
};
|
||||
|
||||
onCloseProblemToggle = () => {
|
||||
this.setState({ closeProblem: !this.state.closeProblem, error: false });
|
||||
}
|
||||
};
|
||||
|
||||
onChangeSelectedSeverity = v => {
|
||||
onChangeSelectedSeverity = (v) => {
|
||||
this.setState({ selectedSeverity: v });
|
||||
};
|
||||
|
||||
dismiss = () => {
|
||||
this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false });
|
||||
this.props.onDismiss();
|
||||
}
|
||||
};
|
||||
|
||||
submit = () => {
|
||||
const { acknowledge, changeSeverity, closeProblem } = this.state;
|
||||
@@ -109,7 +125,7 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||
if (!this.state.value && !actionSelected) {
|
||||
return this.setState({
|
||||
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;
|
||||
|
||||
this.props.onSubmit(ackData).then(() => {
|
||||
this.dismiss();
|
||||
}).catch(err => {
|
||||
const errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || '';
|
||||
this.setState({
|
||||
ackError: errorMessage,
|
||||
loading: false,
|
||||
this.props
|
||||
.onSubmit(ackData)
|
||||
.then(() => {
|
||||
this.dismiss();
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || '';
|
||||
this.setState({
|
||||
ackError: errorMessage,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderActions() {
|
||||
const { canClose } = this.props;
|
||||
|
||||
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
|
||||
translate=""
|
||||
key="change-severity"
|
||||
label="Change severity"
|
||||
description=""
|
||||
value={this.state.changeSeverity}
|
||||
onChange={this.onChangeSeverityToggle}
|
||||
/>,
|
||||
this.state.changeSeverity &&
|
||||
this.state.changeSeverity && (
|
||||
<RadioButtonGroup
|
||||
key="severity"
|
||||
size="sm"
|
||||
options={severityOptions}
|
||||
value={this.state.selectedSeverity}
|
||||
onChange={this.onChangeSelectedSeverity}
|
||||
/>,
|
||||
canClose &&
|
||||
/>
|
||||
),
|
||||
canClose && (
|
||||
<Checkbox
|
||||
translate=""
|
||||
key="close"
|
||||
label="Close problem"
|
||||
disabled={!canClose}
|
||||
value={this.state.closeProblem}
|
||||
onChange={this.onCloseProblemToggle}
|
||||
/>,
|
||||
/>
|
||||
),
|
||||
];
|
||||
|
||||
// <VerticalGroup /> doesn't handle empty elements properly, so don't return it
|
||||
return actions.filter(e => e);
|
||||
return actions.filter((e) => e);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -204,8 +223,8 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||
>
|
||||
<div className={inputGroupClass}>
|
||||
<label className="gf-form-hint">
|
||||
<TextArea className={inputClass}
|
||||
translate=""
|
||||
<TextArea
|
||||
className={inputClass}
|
||||
type="text"
|
||||
name="message"
|
||||
placeholder="Message"
|
||||
@@ -213,30 +232,30 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||
autoFocus={true}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyPress}>
|
||||
</TextArea>
|
||||
onKeyDown={this.handleKeyPress}
|
||||
></TextArea>
|
||||
<small className={inputHintClass}>Press Enter to submit</small>
|
||||
{this.state.error &&
|
||||
<small className={inputErrorClass}>{this.state.errorMessage}</small>
|
||||
}
|
||||
{this.state.error && <small className={inputErrorClass}>{this.state.errorMessage}</small>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<VerticalGroup>
|
||||
{this.renderActions()}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup>{this.renderActions()}</VerticalGroup>
|
||||
</div>
|
||||
|
||||
{this.state.ackError &&
|
||||
{this.state.ackError && (
|
||||
<div className="gf-form ack-request-error">
|
||||
<span className={styles.ackError}>{this.state.ackError}</span>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<div className="gf-form-button-row text-center">
|
||||
<Button variant="primary" onClick={this.submit}>Update</Button>
|
||||
<Button variant="secondary" onClick={this.dismiss}>Cancel</Button>
|
||||
<Button variant="primary" onClick={this.submit}>
|
||||
Update
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.dismiss}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user