Build plugin with grafana toolkit (#1539)

* Use grafana toolkit template for building plugin

* Fix linter and type errors

* Update styles building

* Fix sass deprecation warning

* Remove empty js files produced by webpack building sass

* Fix signing script

* Replace classnames with cx

* Fix data source config page

* Use custom webpack config instead of overriding original one

* Use gpx_ prefix for plugin executable

* Remove unused configs

* Roll back react hooks dependencies usage

* Move plugin-specific ts config to root config file

* Temporary do not use rst2html for function description tooltip

* Remove unused code

* remove unused dependencies

* update react table dependency

* Migrate tests to typescript

* remove unused dependencies

* Remove old webpack configs

* Add sign target to makefile

* Add magefile

* Update CI test job

* Update go packages

* Update build instructions

* Downgrade go version to 1.18

* Fix go version in ci

* Fix metric picker

* Add comment to webpack config

* remove angular mocks

* update bra config

* Rename datasource-zabbix to datasource (fix mage build)

* Add instructions for building backend with mage

* Fix webpack targets

* Fix ci backend tests

* Add initial e2e tests

* Fix e2e ci tests

* Update docker compose for cypress tests

* build grafana docker image

* Fix docker stop task

* CI: add Grafana compatibility check
This commit is contained in:
Alexander Zobnin
2022-12-09 14:14:34 +03:00
committed by GitHub
parent 26ed740945
commit e3e896742b
136 changed files with 5765 additions and 4636 deletions

View File

@@ -1,10 +1,10 @@
import React, { FC } from 'react';
import { locationService } from '@grafana/runtime';
import { ExploreUrlState, TimeRange, urlUtil } from '@grafana/data';
import { MODE_ITEMID, MODE_METRICS } from '../../datasource-zabbix/constants';
import { MODE_ITEMID, MODE_METRICS } from '../../datasource/constants';
import { ActionButton } from '../ActionButton/ActionButton';
import { expandItemName } from '../../datasource-zabbix/utils';
import { ProblemDTO } from '../../datasource-zabbix/types';
import { expandItemName } from '../../datasource/utils';
import { ProblemDTO } from '../../datasource/types';
interface Props {
problem: ProblemDTO;

View File

@@ -1,14 +1,14 @@
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 { ClickOutsideWrapper, Input, Spinner, useStyles2 } from '@grafana/ui';
import { MetricPickerMenu } from './MetricPickerMenu';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { isRegex } from '../../datasource-zabbix/utils';
import { isRegex } from '../../datasource/utils';
export interface Props {
value: string;
isLoading?: boolean;
options: SelectableValue<string>[];
options: Array<SelectableValue<string>>;
width?: number;
onChange: (value: string) => void;
}
@@ -18,7 +18,7 @@ export const MetricPicker = ({ value, options, isLoading, width, onChange }: Pro
const [query, setQuery] = useState(value);
const [filteredOptions, setFilteredOptions] = useState(options);
const [selectedOptionIdx, setSelectedOptionIdx] = useState(-1);
const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 });
const [offset] = useState({ vertical: 0, horizontal: 0 });
const ref = useRef<HTMLDivElement>(null);
const customStyles = useStyles2(getStyles);
@@ -50,7 +50,7 @@ export const MetricPicker = ({ value, options, isLoading, width, onChange }: Pro
const newQuery = v?.currentTarget?.value;
if (newQuery) {
setQuery(newQuery);
if (value != newQuery) {
if (value !== newQuery) {
const filtered = options.filter(
(option) =>
option.value?.toLowerCase().includes(newQuery.toLowerCase()) ||
@@ -74,10 +74,7 @@ export const MetricPicker = ({ value, options, isLoading, width, onChange }: Pro
};
const onBlurInternal = () => {
if (!isOpen) {
// Only call if menu isn't opened
onChange(query);
}
onChange(query);
};
const onKeyDown = (e: React.KeyboardEvent) => {

View File

@@ -5,7 +5,7 @@ import { CustomScrollbar, getSelectStyles, Icon, Tooltip, useStyles2, useTheme2
import { MENU_MAX_HEIGHT } from './constants';
interface Props {
options: SelectableValue<string>[];
options: Array<SelectableValue<string>>;
onSelect: (option: SelectableValue<string>) => void;
offset: { vertical: number; horizontal: number };
minWidth?: number;

View File

@@ -10,7 +10,7 @@ import { MetricPicker } from '../../components';
import { getVariableOptions } from './QueryEditor/utils';
import { prepareAnnotation } from '../migrations';
const severityOptions: SelectableValue<number>[] = [
const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' },
{ value: 1, label: 'Information' },
{ value: 2, label: 'Warning' },
@@ -46,7 +46,7 @@ export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasour
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
@@ -65,7 +65,7 @@ export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasour
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) => ({
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));

View File

@@ -44,23 +44,26 @@ export const ConfigEditor = (props: Props) => {
if (options.jsonData.dbConnectionEnable) {
if (!options.jsonData.dbConnectionDatasourceId) {
const dsName = options.jsonData.dbConnectionDatasourceName;
getDataSourceSrv().get(dsName)
.then(ds => {
if (ds) {
const selectedDs = getDirectDBDatasources().find(dsOption => dsOption.id === ds.id);
setSelectedDBDatasource({ label: selectedDs.name, value: selectedDs.id });
setCurrentDSType(selectedDs.type);
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
dbConnectionDatasourceId: ds.id,
},
});
}
});
getDataSourceSrv()
.get(dsName)
.then((ds) => {
if (ds) {
const selectedDs = getDirectDBDatasources().find((dsOption) => dsOption.id === ds.id);
setSelectedDBDatasource({ label: selectedDs.name, value: selectedDs.id });
setCurrentDSType(selectedDs.type);
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
dbConnectionDatasourceId: ds.id,
},
});
}
});
} else {
const selectedDs = getDirectDBDatasources().find(dsOption => dsOption.id === options.jsonData.dbConnectionDatasourceId);
const selectedDs = getDirectDBDatasources().find(
(dsOption) => dsOption.id === options.jsonData.dbConnectionDatasourceId
);
setSelectedDBDatasource({ label: selectedDs.name, value: selectedDs.id });
setCurrentDSType(selectedDs.type);
}
@@ -89,7 +92,7 @@ export const ConfigEditor = (props: Props) => {
/>
</div>
<div className="gf-form max-width-25">
{options.secureJsonFields?.password ?
{options.secureJsonFields?.password ? (
<>
<FormField
labelWidth={7}
@@ -100,7 +103,8 @@ export const ConfigEditor = (props: Props) => {
placeholder="Configured"
/>
<Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button>
</> :
</>
) : (
<FormField
labelWidth={7}
inputWidth={15}
@@ -110,7 +114,7 @@ export const ConfigEditor = (props: Props) => {
onChange={secureJsonDataChangeHandler('password', options, onOptionsChange)}
required
/>
}
)}
</div>
<Switch
label="Trends"
@@ -118,34 +122,34 @@ export const ConfigEditor = (props: Props) => {
checked={options.jsonData.trends}
onChange={jsonDataSwitchHandler('trends', options, onOptionsChange)}
/>
{options.jsonData.trends &&
<>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="After"
value={options.jsonData.trendsFrom || ''}
placeholder="7d"
onChange={jsonDataChangeHandler('trendsFrom', options, onOptionsChange)}
tooltip="Time after which trends will be used.
{options.jsonData.trends && (
<>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="After"
value={options.jsonData.trendsFrom || ''}
placeholder="7d"
onChange={jsonDataChangeHandler('trendsFrom', options, onOptionsChange)}
tooltip="Time after which trends will be used.
Best practice is to set this value to your history storage period (7d, 30d, etc)."
/>
</div>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="Range"
value={options.jsonData.trendsRange || ''}
placeholder="4d"
onChange={jsonDataChangeHandler('trendsRange', options, onOptionsChange)}
tooltip="Time range width after which trends will be used instead of history.
/>
</div>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="Range"
value={options.jsonData.trendsRange || ''}
placeholder="4d"
onChange={jsonDataChangeHandler('trendsRange', options, onOptionsChange)}
tooltip="Time range width after which trends will be used instead of history.
It's better to set this value in range of 4 to 7 days to prevent loading large amount of history data."
/>
</div>
</>
}
/>
</div>
</>
)}
<div className="gf-form">
<FormField
labelWidth={7}
@@ -183,33 +187,38 @@ export const ConfigEditor = (props: Props) => {
checked={options.jsonData.dbConnectionEnable}
onChange={jsonDataSwitchHandler('dbConnectionEnable', options, onOptionsChange)}
/>
{options.jsonData.dbConnectionEnable &&
<>
<div className="gf-form">
<InlineFormLabel width={9}>Data Source</InlineFormLabel>
<Select
width={32}
options={getDirectDBDSOptions()}
value={selectedDBDatasource}
onChange={directDBDatasourceChanegeHandler(options, onOptionsChange, setSelectedDBDatasource, setCurrentDSType)}
/>
</div>
{currentDSType === 'influxdb' &&
<div className="gf-form">
<FormField
labelWidth={9}
inputWidth={16}
label="Retention Policy"
value={options.jsonData.dbConnectionRetentionPolicy || ''}
placeholder="Retention policy name"
onChange={jsonDataChangeHandler('dbConnectionRetentionPolicy', options, onOptionsChange)}
tooltip="Specify retention policy name for fetching long-term stored data (optional).
{options.jsonData.dbConnectionEnable && (
<>
<div className="gf-form">
<InlineFormLabel width={9}>Data Source</InlineFormLabel>
<Select
width={32}
options={getDirectDBDSOptions()}
value={selectedDBDatasource}
onChange={directDBDatasourceChanegeHandler(
options,
onOptionsChange,
setSelectedDBDatasource,
setCurrentDSType
)}
/>
</div>
{currentDSType === 'influxdb' && (
<div className="gf-form">
<FormField
labelWidth={9}
inputWidth={16}
label="Retention Policy"
value={options.jsonData.dbConnectionRetentionPolicy || ''}
placeholder="Retention policy name"
onChange={jsonDataChangeHandler('dbConnectionRetentionPolicy', options, onOptionsChange)}
tooltip="Specify retention policy name for fetching long-term stored data (optional).
Leave it blank if only default retention policy used."
/>
</div>
}
</>
}
/>
</div>
)}
</>
)}
</div>
<div className="gf-form-group">
@@ -235,98 +244,102 @@ export const ConfigEditor = (props: Props) => {
);
};
const jsonDataChangeHandler = (
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) => (
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>
) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: event.currentTarget.value,
},
});
};
const jsonDataChangeHandler =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: event.currentTarget.value,
},
});
};
const jsonDataSwitchHandler = (
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) => (
event: React.SyntheticEvent<HTMLInputElement>
) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: (event.target as HTMLInputElement).checked,
},
});
};
const jsonDataSwitchHandler =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLInputElement>) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: (event.target as HTMLInputElement).checked,
},
});
};
const secureJsonDataChangeHandler = (
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) => (
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>
) => {
onChange({
...value,
secureJsonData: {
...value.secureJsonData,
[key]: event.currentTarget.value,
},
});
};
const secureJsonDataChangeHandler =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => {
onChange({
...value,
secureJsonData: {
...value.secureJsonData,
[key]: event.currentTarget.value,
},
});
};
const resetSecureJsonField = (
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) => (
event: React.SyntheticEvent<HTMLButtonElement>
) => {
onChange({
...value,
secureJsonFields: {
...value.secureJsonFields,
[key]: false,
},
});
};
const resetSecureJsonField =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLButtonElement>) => {
onChange({
...value,
secureJsonFields: {
...value.secureJsonFields,
[key]: false,
},
});
};
const directDBDatasourceChanegeHandler = (
options: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange'],
setSelectedDS: React.Dispatch<any>,
setSelectedDSType: React.Dispatch<any>,
) => (
value: SelectableValue<number>
) => {
const selectedDs = getDirectDBDatasources().find(dsOption => dsOption.id === value.value);
setSelectedDS({ label: selectedDs.name, value: selectedDs.id });
setSelectedDSType(selectedDs.type);
onChange({
...options,
jsonData: {
...options.jsonData,
dbConnectionDatasourceId: value.value
},
});
};
const directDBDatasourceChanegeHandler =
(
options: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange'],
setSelectedDS: React.Dispatch<any>,
setSelectedDSType: React.Dispatch<any>
) =>
(value: SelectableValue<number>) => {
const selectedDs = getDirectDBDatasources().find((dsOption) => dsOption.id === value.value);
setSelectedDS({ label: selectedDs.name, value: selectedDs.id });
setSelectedDSType(selectedDs.type);
onChange({
...options,
jsonData: {
...options.jsonData,
dbConnectionDatasourceId: value.value,
},
});
};
const getDirectDBDatasources = () => {
let dsList = (getDataSourceSrv() as any).getAll();
dsList = dsList.filter(ds => SUPPORTED_SQL_DS.includes(ds.type));
dsList = dsList.filter((ds) => SUPPORTED_SQL_DS.includes(ds.type));
return dsList;
};
const getDirectDBDSOptions = () => {
const dsList = getDirectDBDatasources();
const dsOpts: Array<SelectableValue<number>> = dsList.map(ds => ({ label: ds.name, value: ds.id, description: ds.type }));
const dsOpts: Array<SelectableValue<number>> = dsList.map((ds) => ({
label: ds.name,
value: ds.id,
description: ds.type,
}));
return dsOpts;
};

View File

@@ -1,22 +1,7 @@
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 React, { useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, ClickOutsideWrapper, Icon, Input, Menu, useStyles2, useTheme2 } from '@grafana/ui';
import { FuncDef } from '../../types';
import { getCategories } from '../../metricFunctions';

View File

@@ -1,5 +1,5 @@
import React, { Suspense } from 'react';
import { Icon, Tooltip } from '@grafana/ui';
import React from 'react';
import { Icon } from '@grafana/ui';
import { MetricFunc } from '../../types';
const DOCS_FUNC_REF_URL = 'https://alexanderzobnin.github.io/grafana-zabbix/reference/functions/';
@@ -10,30 +10,7 @@ export interface FunctionEditorControlsProps {
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"

View File

@@ -4,7 +4,7 @@ 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 { ShowProblemTypes, ZabbixDSOptions, ZabbixMetricsQuery, ZabbixQueryOptions } from '../types';
import { MetricsQueryEditor } from './QueryEditor/MetricsQueryEditor';
import { QueryFunctionsEditor } from './QueryEditor/QueryFunctionsEditor';
import { QueryOptionsEditor } from './QueryEditor/QueryOptionsEditor';
@@ -136,10 +136,6 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
onChangeInternal({ ...query, options });
};
const getSelectableValue = (value: string): SelectableValue<string> => {
return { value, label: value };
};
const renderMetricsEditor = () => {
return (
<>

View File

@@ -10,7 +10,7 @@ import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const slaPropertyList: SelectableValue<string>[] = [
const slaPropertyList: Array<SelectableValue<string>> = [
{ label: 'Status', value: 'status' },
{ label: 'SLA', value: 'sla' },
{ label: 'OK time', value: 'okTime' },
@@ -18,7 +18,7 @@ const slaPropertyList: SelectableValue<string>[] = [
{ label: 'Down time', value: 'downtimeTime' },
];
const slaIntervals: SelectableValue<string>[] = [
const slaIntervals: Array<SelectableValue<string>> = [
{ label: 'No interval', value: 'none' },
{ label: 'Auto', value: 'auto' },
{ label: '1 hour', value: '1h' },

View File

@@ -35,7 +35,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
@@ -54,7 +54,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
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) => ({
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
@@ -78,7 +78,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
showDisabledItems: query.options.showDisabledItems,
};
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
let itemOptions: SelectableValue<string>[] = items?.map((item) => ({
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name,
label: item.name,
}));

View File

@@ -10,13 +10,13 @@ import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const showProblemsOptions: SelectableValue<string>[] = [
const showProblemsOptions: Array<SelectableValue<string>> = [
{ label: 'Problems', value: 'problems' },
{ label: 'Recent problems', value: 'recent' },
{ label: 'History', value: 'history' },
];
const severityOptions: SelectableValue<number>[] = [
const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' },
{ value: 1, label: 'Information' },
{ value: 2, label: 'Warning' },
@@ -50,7 +50,7 @@ export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
@@ -69,7 +69,7 @@ export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
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) => ({
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));

View File

@@ -34,7 +34,7 @@ export const QueryFunctionsEditor = ({ query, onChange }: Props) => {
};
const onRemoveFunc = (func: MetricFunc) => {
const functions = query.functions?.filter((f) => f != func);
const functions = query.functions?.filter((f) => f !== func);
onChange({ ...query, functions });
};

View File

@@ -14,13 +14,13 @@ import {
import * as c from '../../constants';
import { ZabbixQueryOptions } from '../../types';
const ackOptions: SelectableValue<number>[] = [
const ackOptions: Array<SelectableValue<number>> = [
{ label: 'all triggers', value: 2 },
{ label: 'unacknowledged', value: 0 },
{ label: 'acknowledged', value: 1 },
];
const sortOptions: SelectableValue<string>[] = [
const sortOptions: Array<SelectableValue<string>> = [
{ label: 'Default', value: 'default' },
{ label: 'Last change', value: 'lastchange' },
{ label: 'Severity', value: 'severity' },

View File

@@ -35,7 +35,7 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
@@ -54,7 +54,7 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
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) => ({
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
@@ -78,7 +78,7 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
showDisabledItems: query.options.showDisabledItems,
};
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
let itemOptions: SelectableValue<string>[] = items?.map((item) => ({
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name,
label: item.name,
}));

View File

@@ -10,7 +10,7 @@ import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const severityOptions: SelectableValue<number>[] = [
const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' },
{ value: 1, label: 'Information' },
{ value: 2, label: 'Warning' },
@@ -44,7 +44,7 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: SelectableValue<string>[] = hosts?.map((host) => ({
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
@@ -63,7 +63,7 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
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) => ({
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));

View File

@@ -69,7 +69,7 @@ function setAliasByRegex(alias: string, frame: DataFrame) {
valueField.config.displayNameFromDS = extractText(valueField.config?.displayNameFromDS, alias);
}
frame.name = extractText(frame.name, alias);
} catch (error) {
} catch (error: any) {
console.error('Failed to apply RegExp:', error?.message || error);
}
return frame;
@@ -79,7 +79,7 @@ function setAliasByRegex(alias: string, frame: DataFrame) {
if (field.type !== FieldType.time) {
try {
field.config.displayNameFromDS = extractText(field.config?.displayNameFromDS, alias);
} catch (error) {
} catch (error: any) {
console.error('Failed to apply RegExp:', error?.message || error);
}
}

View File

@@ -48,11 +48,9 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
replaceTemplateVars: (target: any, scopedVars?: any) => any;
/** @ngInject */
constructor(instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>, private templateSrv) {
constructor(instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>) {
super(instanceSettings);
this.templateSrv = templateSrv;
this.enableDebugLog = config.buildInfo.env === 'development';
this.annotations = {
@@ -61,7 +59,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
};
// Use custom format for template variables
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
const templateSrv = getTemplateSrv();
this.replaceTemplateVars = _.partial(replaceTemplateVars, templateSrv);
// General data source settings
this.datasourceId = instanceSettings.id;
@@ -455,7 +454,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
*/
queryItemIdData(target, timeRange, useTrends, options) {
let itemids = target.itemids;
itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
const templateSrv = getTemplateSrv();
itemids = templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
itemids = _.map(itemids.split(','), (itemid) => itemid.trim());
if (!itemids) {
@@ -631,7 +631,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
title: 'Success',
message: message,
};
} catch (error) {
} catch (error: any) {
if (error instanceof ZabbixAPIError) {
return {
status: 'error',
@@ -824,6 +824,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// Replace template variables
replaceTargetVariables(target, options) {
const templateSrv = getTemplateSrv();
const parts = ['group', 'host', 'application', 'itemTag', 'item'];
_.forEach(parts, (p) => {
if (target[p] && target[p].filter) {
@@ -836,15 +837,15 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
}
if (target.itemids) {
target.itemids = this.templateSrv.replace(target.itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
target.itemids = templateSrv.replace(target.itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
}
_.forEach(target.functions, (func) => {
func.params = _.map(func.params, (param) => {
if (typeof param === 'number') {
return +this.templateSrv.replace(param.toString(), options.scopedVars);
return +templateSrv.replace(param.toString(), options.scopedVars);
} else {
return this.templateSrv.replace(param, options.scopedVars);
return templateSrv.replace(param, options.scopedVars);
}
});
});
@@ -935,7 +936,7 @@ function zabbixItemIdsTemplateFormat(value) {
* $variable -> a|b|c -> /a|b|c/
* /$variable/ -> /a|b|c/ -> /a|b|c/
*/
function replaceTemplateVars(templateSrv, target, scopedVars) {
export function replaceTemplateVars(templateSrv, target, scopedVars) {
let replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
if (target && target !== replacedTarget && !utils.isRegex(replacedTarget)) {
replacedTarget = '/^' + replacedTarget + '$/';

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,16 +1,8 @@
import { DataSourcePlugin } from '@grafana/data';
import { loadPluginCss } from '@grafana/runtime';
import { ZabbixDatasource } from './datasource';
import { QueryEditor } from './components/QueryEditor';
import { ZabbixVariableQueryEditor } from './components/VariableQueryEditor';
import { ConfigEditor } from './components/ConfigEditor';
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',
});
export const plugin = new DataSourcePlugin(ZabbixDatasource)
.setConfigEditor(ConfigEditor)

View File

@@ -2,14 +2,11 @@
"type": "datasource",
"name": "Zabbix",
"id": "alexanderzobnin-zabbix-datasource",
"metrics": true,
"annotations": true,
"backend": true,
"alerting": true,
"executable": "../zabbix-plugin",
"executable": "../gpx_zabbix-plugin",
"includes": [
{
"type": "dashboard",
@@ -27,11 +24,9 @@
"path": "dashboards/zabbix_server_dashboard.json"
}
],
"queryOptions": {
"maxDataPoints": true
},
"info": {
"author": {
"name": "Alexander Zobnin",

View File

@@ -1,5 +1,5 @@
import _ from 'lodash';
import * as utils from '../datasource-zabbix/utils';
import * as utils from './utils';
import { DataFrame, Field, FieldType, ArrayVector } from '@grafana/data';
import { ZBXProblem, ZBXTrigger, ProblemDTO, ZBXEvent } from './types';

View File

@@ -1,24 +1,30 @@
import mocks from '../../test-setup/mocks';
import { ZabbixDatasource, zabbixTemplateFormat } from "../datasource";
import _ from 'lodash';
import { templateSrvMock, datasourceSrvMock } from '../../test-setup/mocks';
import { replaceTemplateVars, ZabbixDatasource, zabbixTemplateFormat } from '../datasource';
import { dateMath } from '@grafana/data';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({ data: { result: '' } }),
fetch: () => ({
toPromise: () => jest.fn().mockResolvedValue({ data: { result: '' } })
jest.mock(
'@grafana/runtime',
() => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({ data: { result: '' } }),
fetch: () => ({
toPromise: () => jest.fn().mockResolvedValue({ data: { result: '' } }),
}),
}),
getTemplateSrv: () => ({
replace: jest.fn().mockImplementation((query) => query),
}),
}),
loadPluginCss: () => {
},
}), { virtual: true });
{ virtual: true }
);
jest.mock('../components/AnnotationQueryEditor', () => ({
AnnotationQueryEditor: () => {},
}));
describe('ZabbixDatasource', () => {
let ctx = {};
let ctx: any = {};
beforeEach(() => {
ctx.instanceSettings = {
@@ -29,82 +35,89 @@ describe('ZabbixDatasource', () => {
trends: true,
trendsFrom: '14d',
trendsRange: '7d',
dbConnectionEnable: false
}
dbConnectionEnable: false,
},
};
ctx.options = {
targets: [
{
group: { filter: "" },
host: { filter: "" },
application: { filter: "" },
item: { filter: "" }
}
group: { filter: '' },
host: { filter: '' },
application: { filter: '' },
item: { filter: '' },
},
],
range: {
from: dateMath.parse('now-1h'),
to: dateMath.parse('now')
}
to: dateMath.parse('now'),
},
};
ctx.templateSrv = mocks.templateSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock;
ctx.datasourceSrv = datasourceSrvMock;
ctx.ds = new ZabbixDatasource(ctx.instanceSettings, ctx.templateSrv);
ctx.ds = new ZabbixDatasource(ctx.instanceSettings);
ctx.ds.templateSrv = templateSrvMock;
});
describe('When querying text data', () => {
beforeEach(() => {
ctx.ds.replaceTemplateVars = (str) => str;
ctx.ds.zabbix.zabbixAPI.getHistory = jest.fn().mockReturnValue(Promise.resolve([
{ clock: "1500010200", itemid: "10100", ns: "900111000", value: "Linux first" },
{ clock: "1500010300", itemid: "10100", ns: "900111000", value: "Linux 2nd" },
{ clock: "1500010400", itemid: "10100", ns: "900111000", value: "Linux last" }
]));
ctx.ds.zabbix.zabbixAPI.getHistory = jest.fn().mockReturnValue(
Promise.resolve([
{ clock: '1500010200', itemid: '10100', ns: '900111000', value: 'Linux first' },
{ clock: '1500010300', itemid: '10100', ns: '900111000', value: 'Linux 2nd' },
{ clock: '1500010400', itemid: '10100', ns: '900111000', value: 'Linux last' },
])
);
ctx.ds.zabbix.getItemsFromTarget = jest.fn().mockReturnValue(Promise.resolve([
ctx.ds.zabbix.getItemsFromTarget = jest.fn().mockReturnValue(
Promise.resolve([
{
hosts: [{ hostid: '10001', name: 'Zabbix server' }],
itemid: '10100',
name: 'System information',
key_: 'system.uname',
},
])
);
ctx.options.targets = [
{
hosts: [{ hostid: "10001", name: "Zabbix server" }],
itemid: "10100",
name: "System information",
key_: "system.uname",
}
]));
ctx.options.targets = [{
group: { filter: "" },
host: { filter: "Zabbix server" },
application: { filter: "" },
item: { filter: "System information" },
textFilter: "",
useCaptureGroups: true,
queryType: 2,
resultFormat: "table",
options: {
skipEmptyValues: false
}
}];
group: { filter: '' },
host: { filter: 'Zabbix server' },
application: { filter: '' },
item: { filter: 'System information' },
textFilter: '',
useCaptureGroups: true,
queryType: 2,
resultFormat: 'table',
options: {
skipEmptyValues: false,
},
},
];
});
it('should return data in table format', (done) => {
ctx.ds.query(ctx.options).then(result => {
ctx.ds.query(ctx.options).then((result) => {
expect(result.data.length).toBe(1);
let tableData = result.data[0];
expect(tableData.columns).toEqual([
{ text: 'Host' }, { text: 'Item' }, { text: 'Key' }, { text: 'Last value' }
]);
expect(tableData.rows).toEqual([
['Zabbix server', 'System information', 'system.uname', 'Linux last']
{ text: 'Host' },
{ text: 'Item' },
{ text: 'Key' },
{ text: 'Last value' },
]);
expect(tableData.rows).toEqual([['Zabbix server', 'System information', 'system.uname', 'Linux last']]);
done();
});
});
it('should extract value if regex with capture group is used', (done) => {
ctx.options.targets[0].textFilter = "Linux (.*)";
ctx.ds.query(ctx.options).then(result => {
ctx.options.targets[0].textFilter = 'Linux (.*)';
ctx.ds.query(ctx.options).then((result) => {
let tableData = result.data[0];
expect(tableData.rows[0][3]).toEqual('last');
done();
@@ -112,26 +125,34 @@ describe('ZabbixDatasource', () => {
});
it('should skip item when last value is empty', () => {
ctx.ds.zabbix.getItemsFromTarget = jest.fn().mockReturnValue(Promise.resolve([
{
hosts: [{ hostid: "10001", name: "Zabbix server" }],
itemid: "10100", name: "System information", key_: "system.uname"
},
{
hosts: [{ hostid: "10002", name: "Server02" }],
itemid: "90109", name: "System information", key_: "system.uname"
}
]));
ctx.ds.zabbix.getItemsFromTarget = jest.fn().mockReturnValue(
Promise.resolve([
{
hosts: [{ hostid: '10001', name: 'Zabbix server' }],
itemid: '10100',
name: 'System information',
key_: 'system.uname',
},
{
hosts: [{ hostid: '10002', name: 'Server02' }],
itemid: '90109',
name: 'System information',
key_: 'system.uname',
},
])
);
ctx.options.targets[0].options.skipEmptyValues = true;
ctx.ds.zabbix.getHistory = jest.fn().mockReturnValue(Promise.resolve([
{ clock: "1500010200", itemid: "10100", ns: "900111000", value: "Linux first" },
{ clock: "1500010300", itemid: "10100", ns: "900111000", value: "Linux 2nd" },
{ clock: "1500010400", itemid: "10100", ns: "900111000", value: "Linux last" },
{ clock: "1500010200", itemid: "90109", ns: "900111000", value: "Non empty value" },
{ clock: "1500010500", itemid: "90109", ns: "900111000", value: "" }
]));
return ctx.ds.query(ctx.options).then(result => {
ctx.ds.zabbix.getHistory = jest.fn().mockReturnValue(
Promise.resolve([
{ clock: '1500010200', itemid: '10100', ns: '900111000', value: 'Linux first' },
{ clock: '1500010300', itemid: '10100', ns: '900111000', value: 'Linux 2nd' },
{ clock: '1500010400', itemid: '10100', ns: '900111000', value: 'Linux last' },
{ clock: '1500010200', itemid: '90109', ns: '900111000', value: 'Non empty value' },
{ clock: '1500010500', itemid: '90109', ns: '900111000', value: '' },
])
);
return ctx.ds.query(ctx.options).then((result) => {
let tableData = result.data[0];
expect(tableData.rows.length).toBe(1);
expect(tableData.rows[0][3]).toEqual('Linux last');
@@ -140,11 +161,10 @@ describe('ZabbixDatasource', () => {
});
describe('When replacing template variables', () => {
function testReplacingVariable(target, varValue, expectedResult, done) {
ctx.ds.templateSrv.replace = () => {
return zabbixTemplateFormat(varValue);
};
ctx.ds.replaceTemplateVars = _.partial(replaceTemplateVars, {
replace: jest.fn((target) => zabbixTemplateFormat(varValue)),
});
let result = ctx.ds.replaceTemplateVars(target);
expect(result).toBe(expectedResult);
@@ -198,7 +218,7 @@ describe('ZabbixDatasource', () => {
getGroups: jest.fn().mockReturnValue(Promise.resolve([])),
getHosts: jest.fn().mockReturnValue(Promise.resolve([])),
getApps: jest.fn().mockReturnValue(Promise.resolve([])),
getItems: jest.fn().mockReturnValue(Promise.resolve([]))
getItems: jest.fn().mockReturnValue(Promise.resolve([])),
};
});
@@ -218,7 +238,7 @@ describe('ZabbixDatasource', () => {
});
it('should return empty list for empty query', (done) => {
ctx.ds.metricFindQuery('').then(result => {
ctx.ds.metricFindQuery('').then((result) => {
expect(ctx.ds.zabbix.getGroups).toBeCalledTimes(0);
ctx.ds.zabbix.getGroups.mockClear();
@@ -248,7 +268,7 @@ describe('ZabbixDatasource', () => {
{ query: '*.*.*', expect: ['/.*/', '/.*/', '/.*/'] },
{ query: '.*.', expect: ['', '/.*/', ''] },
{ query: 'Backend.backend01.*', expect: ['Backend', 'backend01', '/.*/'] },
{ query: 'Back*.*.', expect: ['Back*', '/.*/', ''] }
{ query: 'Back*.*.', expect: ['Back*', '/.*/', ''] },
];
for (const test of tests) {
@@ -264,13 +284,18 @@ describe('ZabbixDatasource', () => {
{ query: '*.*.*.*', expect: ['/.*/', '/.*/', '', null, '/.*/'] },
{ query: '.*.*.*', expect: ['', '/.*/', '', null, '/.*/'] },
{ query: 'Backend.backend01.*.*', expect: ['Backend', 'backend01', '', null, '/.*/'] },
{ query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu', null, '/.*/'] }
{ query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu', null, '/.*/'] },
];
for (const test of tests) {
ctx.ds.metricFindQuery(test.query);
expect(ctx.ds.zabbix.getItems)
.toBeCalledWith(test.expect[0], test.expect[1], test.expect[2], test.expect[3], test.expect[4]);
expect(ctx.ds.zabbix.getItems).toBeCalledWith(
test.expect[0],
test.expect[1],
test.expect[2],
test.expect[3],
test.expect[4]
);
ctx.ds.zabbix.getItems.mockClear();
}
done();

View File

@@ -3,14 +3,12 @@ import { compactQuery } from '../utils';
jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: jest.fn(() => ({
get: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
get: jest.fn().mockResolvedValue({ id: 42, name: 'InfluxDB DS', meta: {} }),
})),
}));
describe('InfluxDBConnector', () => {
let ctx = {};
let ctx: any = {};
beforeEach(() => {
ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
@@ -20,7 +18,8 @@ describe('InfluxDBConnector', () => {
itemids: ['123', '234'],
range: { timeFrom: 15000, timeTill: 15100 },
intervalSec: 5,
table: 'history', aggFunction: 'MAX'
table: 'history',
aggFunction: 'MAX',
};
});
@@ -57,9 +56,7 @@ describe('InfluxDBConnector', () => {
it('should query proper table depending on item type', () => {
const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 };
const items = [
{ itemid: '123', value_type: 3 }
];
const items = [{ itemid: '123', value_type: 3 }];
const expectedQuery = compactQuery(`SELECT MEAN("value")
FROM "history_uint"
WHERE ("itemid" = '123')
@@ -97,9 +94,7 @@ describe('InfluxDBConnector', () => {
ctx.influxDBConnector.retentionPolicy = '';
const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 };
const items = [
{ itemid: '123', value_type: 3 }
];
const items = [{ itemid: '123', value_type: 3 }];
const expectedQuery = compactQuery(`SELECT MEAN("value")
FROM "history_uint"
WHERE ("itemid" = '123')
@@ -114,9 +109,7 @@ describe('InfluxDBConnector', () => {
it('should use retention policy name for trends query if it was set', () => {
const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 };
const items = [
{ itemid: '123', value_type: 3 }
];
const items = [{ itemid: '123', value_type: 3 }];
const expectedQuery = compactQuery(`SELECT MEAN("value_avg")
FROM "longterm"."history_uint"
WHERE ("itemid" = '123')
@@ -131,9 +124,7 @@ describe('InfluxDBConnector', () => {
it('should use proper value column if retention policy set (trends used)', () => {
const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000, consolidateBy: 'max' };
const items = [
{ itemid: '123', value_type: 3 }
];
const items = [{ itemid: '123', value_type: 3 }];
const expectedQuery = compactQuery(`SELECT MAX("value_max")
FROM "longterm"."history_uint"
WHERE ("itemid" = '123')

View File

@@ -2,15 +2,15 @@ import _ from 'lodash';
import { migrateDSConfig, DS_CONFIG_SCHEMA } from '../migrations';
describe('Migrations', () => {
let ctx = {};
let ctx: any = {};
describe('When migrating datasource config', () => {
beforeEach(() => {
ctx.jsonData = {
dbConnection: {
enable: true,
datasourceId: 1
}
datasourceId: 1,
},
};
});
@@ -19,7 +19,7 @@ describe('Migrations', () => {
expect(ctx.jsonData).toMatchObject({
dbConnectionEnable: true,
dbConnectionDatasourceId: 1,
schema: DS_CONFIG_SCHEMA
schema: DS_CONFIG_SCHEMA,
});
});
@@ -27,13 +27,13 @@ describe('Migrations', () => {
ctx.jsonData = {
futureOptionOne: 'foo',
futureOptionTwo: 'bar',
schema: DS_CONFIG_SCHEMA
schema: DS_CONFIG_SCHEMA,
};
migrateDSConfig(ctx.jsonData);
expect(ctx.jsonData).toMatchObject({
futureOptionOne: 'foo',
futureOptionTwo: 'bar',
schema: DS_CONFIG_SCHEMA
schema: DS_CONFIG_SCHEMA,
});
expect(ctx.jsonData.dbConnectionEnable).toBeUndefined();
expect(ctx.jsonData.dbConnectionDatasourceId).toBeUndefined();
@@ -55,7 +55,7 @@ describe('Migrations', () => {
disableReadOnlyUsersAck: true,
dbConnectionEnable: true,
dbConnectionDatasourceName: 'MySQL Zabbix',
dbConnectionRetentionPolicy: 'one_year'
dbConnectionRetentionPolicy: 'one_year',
};
});

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
// eslint-disable-next-line
import moment from 'moment';
import * as c from './constants';
import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types';
@@ -507,12 +508,12 @@ export function isProblemsDataFrame(data: DataFrame): boolean {
}
// Swap n and k elements.
export function swap<T>(list: Array<T>, n: number, k: number): Array<T> {
export function swap<T>(list: T[], n: number, k: number): 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);
const newList: T[] = new Array(list.length);
for (let i = 0; i < list.length; i++) {
if (i === n) {
newList[i] = list[k];

View File

@@ -31,14 +31,6 @@ export const consolidateByTrendColumns = {
sum: 'num*value_avg', // sum of sums inside the one-hour trend period
};
export interface IDBConnector {
getHistory(): any;
getTrends(): any;
testDataSource(): any;
}
/**
* Base class for external history database connectors. Subclasses should implement `getHistory()`, `getTrends()` and
* `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API.
@@ -47,13 +39,13 @@ export class DBConnector {
protected datasourceId: any;
private datasourceName: any;
protected datasourceTypeId: any;
private datasourceTypeName: any;
// private datasourceTypeName: any;
constructor(options) {
this.datasourceId = options.datasourceId;
this.datasourceName = options.datasourceName;
this.datasourceTypeId = null;
this.datasourceTypeName = null;
// this.datasourceTypeName = null;
}
static loadDatasource(dsId, dsName) {
@@ -74,7 +66,7 @@ export class DBConnector {
loadDBDataSource() {
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName).then((ds) => {
this.datasourceTypeId = ds.meta.id;
this.datasourceTypeName = ds.meta.name;
// this.datasourceTypeName = ds.meta.name;
if (!this.datasourceName) {
this.datasourceName = ds.name;
}

View File

@@ -2,12 +2,10 @@ import {
ArrayVector,
DataFrame,
dataFrameToJSON,
DataSourceApi,
Field,
FieldType,
MutableDataFrame,
TIME_SERIES_TIME_FIELD_NAME,
toDataFrame,
} from '@grafana/data';
import _ from 'lodash';
import { compactQuery } from '../../../utils';

View File

@@ -20,7 +20,7 @@ const roundInterval: (interval: number) => number = rangeUtil?.roundInterval ||
*/
export class ZabbixAPIConnector {
backendAPIUrl: string;
requestOptions: { basicAuth: any; withCredentials: boolean; };
requestOptions: { basicAuth: any; withCredentials: boolean };
getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>;
version: string;
getVersionPromise: Promise<string>;
@@ -32,7 +32,7 @@ export class ZabbixAPIConnector {
this.requestOptions = {
basicAuth: basicAuth,
withCredentials: withCredentials
withCredentials: withCredentials,
};
this.getTrend = this.getTrend_ZBXNEXT1193;
@@ -58,7 +58,7 @@ export class ZabbixAPIConnector {
url: this.backendAPIUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
hideFromInspector: false,
data: {
@@ -90,7 +90,7 @@ export class ZabbixAPIConnector {
initVersion(): Promise<string> {
if (!this.getVersionPromise) {
this.getVersionPromise = Promise.resolve(
this.getVersion().then(version => {
this.getVersion().then((version) => {
if (version) {
console.log(`Zabbix version detected: ${version}`);
} else {
@@ -122,7 +122,7 @@ export class ZabbixAPIConnector {
const params: any = {
eventids: eventid,
message: message,
action: action
action: action,
};
if (severity !== undefined) {
@@ -136,7 +136,7 @@ export class ZabbixAPIConnector {
const params = {
output: ['name', 'groupid'],
sortfield: 'name',
real_hosts: true
real_hosts: true,
};
return this.request('hostgroup.get', params);
@@ -145,7 +145,7 @@ export class ZabbixAPIConnector {
getHosts(groupids) {
const params: any = {
output: ['hostid', 'name', 'host'],
sortfield: 'name'
sortfield: 'name',
};
if (groupids) {
params.groupids = groupids;
@@ -161,7 +161,7 @@ export class ZabbixAPIConnector {
const params = {
output: 'extend',
hostids: hostids
hostids: hostids,
};
return this.request('application.get', params);
@@ -176,22 +176,11 @@ export class ZabbixAPIConnector {
*/
getItems(hostids, appids, itemtype) {
const params: any = {
output: [
'itemid',
'name',
'key_',
'value_type',
'hostid',
'status',
'state',
'units',
'valuemapid',
'delay'
],
output: ['itemid', 'name', 'key_', 'value_type', 'hostid', 'status', 'state', 'units', 'valuemapid', 'delay'],
sortfield: 'name',
webitems: true,
filter: {},
selectHosts: ['hostid', 'name', 'host']
selectHosts: ['hostid', 'name', 'host'],
};
if (hostids) {
params.hostids = hostids;
@@ -212,41 +201,28 @@ export class ZabbixAPIConnector {
params.selectTags = 'extend';
}
return this.request('item.get', params)
.then(utils.expandItems);
return this.request('item.get', params).then(utils.expandItems);
}
getItemsByIDs(itemids) {
const params: any = {
itemids: itemids,
output: [
'itemid',
'name',
'key_',
'value_type',
'hostid',
'status',
'state',
'units',
'valuemapid',
'delay'
],
output: ['itemid', 'name', 'key_', 'value_type', 'hostid', 'status', 'state', 'units', 'valuemapid', 'delay'],
webitems: true,
selectHosts: ['hostid', 'name']
selectHosts: ['hostid', 'name'],
};
if (this.isZabbix54OrHigher()) {
params.selectTags = 'extend';
}
return this.request('item.get', params)
.then(items => utils.expandItems(items));
return this.request('item.get', params).then((items) => utils.expandItems(items));
}
getMacros(hostids) {
const params = {
output: 'extend',
hostids: hostids
hostids: hostids,
};
return this.request('usermacro.get', params);
@@ -255,7 +231,7 @@ export class ZabbixAPIConnector {
getGlobalMacros() {
const params = {
output: 'extend',
globalmacro: true
globalmacro: true,
};
return this.request('usermacro.get', params);
@@ -264,10 +240,9 @@ export class ZabbixAPIConnector {
getLastValue(itemid) {
const params = {
output: ['lastvalue'],
itemids: itemid
itemids: itemid,
};
return this.request('item.get', params)
.then(items => items.length ? items[0].lastvalue : null);
return this.request('item.get', params).then((items) => (items.length ? items[0].lastvalue : null));
}
/**
@@ -279,7 +254,6 @@ export class ZabbixAPIConnector {
* @return {Array} Array of Zabbix history objects
*/
getHistory(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
@@ -290,7 +264,7 @@ export class ZabbixAPIConnector {
itemids: itemids,
sortfield: 'clock',
sortorder: 'ASC',
time_from: timeFrom
time_from: timeFrom,
};
// Relative queries (e.g. last hour) don't include an end time
@@ -314,7 +288,6 @@ export class ZabbixAPIConnector {
* @return {Array} Array of Zabbix trend objects
*/
getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
@@ -325,7 +298,7 @@ export class ZabbixAPIConnector {
itemids: itemids,
sortfield: 'clock',
sortorder: 'ASC',
time_from: timeFrom
time_from: timeFrom,
};
// Relative queries (e.g. last hour) don't include an end time
@@ -344,13 +317,9 @@ export class ZabbixAPIConnector {
const itemids = _.map(items, 'itemid');
const params: any = {
output: [
'itemid',
'clock',
value_type
],
output: ['itemid', 'clock', value_type],
itemids: itemids,
time_from: time_from
time_from: time_from,
};
// Relative queries (e.g. last hour) don't include an end time
@@ -364,7 +333,7 @@ export class ZabbixAPIConnector {
getITService(serviceids?) {
const params = {
output: 'extend',
serviceids: serviceids
serviceids: serviceids,
};
return this.request('service.get', params);
}
@@ -382,7 +351,7 @@ export class ZabbixAPIConnector {
const params: any = {
serviceids,
intervals
intervals,
};
return this.request('service.getsla', params);
@@ -410,10 +379,10 @@ export class ZabbixAPIConnector {
}
const sla = slaObjects[0];
const periods = intervals.map(interval => ({
period_from: interval.from,
period_to: interval.to,
}));
// const periods = intervals.map(interval => ({
// period_from: interval.from,
// period_to: interval.to,
// }));
const sliParams: any = {
slaid: sla.slaid,
serviceids,
@@ -430,7 +399,7 @@ export class ZabbixAPIConnector {
const slaLikeResponse: any = {};
sliResponse.serviceids.forEach((serviceid) => {
slaLikeResponse[serviceid] = {
sla: []
sla: [],
};
});
sliResponse.sli.forEach((sliItem, i) => {
@@ -440,7 +409,7 @@ export class ZabbixAPIConnector {
okTime: sli.uptime,
sla: sli.sli,
from: sliResponse.periods[i].period_from,
to: sliResponse.periods[i].period_to
to: sliResponse.periods[i].period_to,
});
});
});
@@ -526,13 +495,13 @@ export class ZabbixAPIConnector {
skipDependent: true,
//only_true: true,
filter: {
value: 1
value: 1,
},
selectGroups: ['groupid', 'name'],
selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid'],
selectItems: ['itemid', 'name', 'key_', 'lastvalue'],
selectLastEvent: 'extend',
selectTags: 'extend'
selectTags: 'extend',
};
if (showTriggers === ShowProblemTypes.Problems) {
@@ -617,7 +586,7 @@ export class ZabbixAPIConnector {
select_acknowledges: 'extend',
selectTags: 'extend',
sortfield: 'clock',
sortorder: 'DESC'
sortorder: 'DESC',
};
return this.request('event.get', params);
@@ -626,13 +595,7 @@ export class ZabbixAPIConnector {
getEventAlerts(eventids) {
const params = {
eventids: eventids,
output: [
'alertid',
'eventid',
'message',
'clock',
'error'
],
output: ['alertid', 'eventid', 'message', 'clock', 'error'],
selectUsers: true,
};
@@ -646,11 +609,10 @@ export class ZabbixAPIConnector {
preservekeys: true,
select_acknowledges: 'extend',
sortfield: 'clock',
sortorder: 'DESC'
sortorder: 'DESC',
};
return this.request('event.get', params)
.then(events => {
return this.request('event.get', params).then((events) => {
return _.filter(events, (event) => event.acknowledges.length);
});
}
@@ -668,7 +630,7 @@ export class ZabbixAPIConnector {
// filter: {
// value: 1
// },
selectLastEvent: 'extend'
selectLastEvent: 'extend',
};
if (timeFrom || timeTo) {
@@ -693,7 +655,7 @@ export class ZabbixAPIConnector {
skipDependent: true,
selectLastEvent: 'extend',
selectGroups: 'extend',
selectHosts: ['hostid', 'host', 'name']
selectHosts: ['hostid', 'host', 'name'],
};
if (count && acknowledged !== 0 && acknowledged !== 1) {
@@ -709,8 +671,7 @@ export class ZabbixAPIConnector {
params.lastChangeTill = timeTo;
}
return this.request('trigger.get', params)
.then((triggers) => {
return this.request('trigger.get', params).then((triggers) => {
if (!count || acknowledged === 0 || acknowledged === 1) {
triggers = filterTriggersByAcknowledge(triggers, acknowledged);
if (count) {
@@ -750,7 +711,7 @@ export class ZabbixAPIConnector {
getValueMappings() {
const params = {
output: 'extend',
selectMappings: "extend",
selectMappings: 'extend',
};
return this.request('valuemap.get', params);
@@ -759,9 +720,9 @@ export class ZabbixAPIConnector {
function filterTriggersByAcknowledge(triggers, acknowledged) {
if (acknowledged === 0) {
return _.filter(triggers, (trigger) => trigger.lastEvent.acknowledged === "0");
return _.filter(triggers, (trigger) => trigger.lastEvent.acknowledged === '0');
} else if (acknowledged === 1) {
return _.filter(triggers, (trigger) => trigger.lastEvent.acknowledged === "1");
return _.filter(triggers, (trigger) => trigger.lastEvent.acknowledged === '1');
} else {
return triggers;
}
@@ -785,9 +746,8 @@ function buildSLAIntervals(timeRange, interval) {
for (let i = timeFrom; i <= timeTo - interval; i += interval) {
intervals.push({
from: i,
to: (i + interval)
to: i + interval,
});
}
return intervals;
@@ -802,12 +762,12 @@ export class ZabbixAPIError {
constructor(error: JSONRPCError) {
this.code = error.code || null;
this.name = error.message || "";
this.data = error.data || "";
this.message = "Zabbix API Error: " + this.name + " " + this.data;
this.name = error.message || '';
this.data = error.data || '';
this.message = 'Zabbix API Error: ' + this.name + ' ' + this.data;
}
toString() {
return this.name + " " + this.data;
return this.name + ' ' + this.data;
}
}

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
// eslint-disable-next-line
import moment from 'moment';
import semver from 'semver';
import * as utils from '../utils';

View File

@@ -1,12 +1,9 @@
import { AppPlugin } from '@grafana/data';
import { loadPluginCss } from 'grafana/app/plugins/sdk';
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',
dark: 'plugins/alexanderzobnin-zabbix-app/styles/dark.css',
light: 'plugins/alexanderzobnin-zabbix-app/styles/light.css',
});
export const plugin = new AppPlugin<{}>();

View File

@@ -1,12 +1,11 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React from 'react';
import _ from 'lodash';
import { BusEventBase, BusEventWithPayload, dateMath, PanelProps } from '@grafana/data';
import { DataSourceRef, dateMath, PanelProps } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { useTheme2 } from '@grafana/ui';
import { contextSrv } from 'grafana/app/core/core';
import { ProblemsPanelOptions } from './types';
import { ProblemDTO, ZabbixMetricsQuery, ZBXQueryUpdatedEvent, ZBXTag } from '../datasource-zabbix/types';
import { APIExecuteScriptResponse } from '../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { ProblemsPanelOptions, RTResized } from './types';
import { ProblemDTO, ZabbixMetricsQuery, ZBXQueryUpdatedEvent, ZBXTag } from '../datasource/types';
import { APIExecuteScriptResponse } from '../datasource/zabbix/connectors/zabbix_api/types';
import ProblemList from './components/Problems/Problems';
import { AckProblemData } from './components/AckModal';
import AlertList from './components/AlertList/AlertList';
@@ -18,7 +17,6 @@ interface ProblemsPanelProps extends PanelProps<ProblemsPanelOptions> {}
export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
const { data, options, timeRange, onOptionsChange } = props;
const { layout, showTriggers, triggerSeverity, sortProblems } = options;
const theme = useTheme2();
const prepareProblems = () => {
const problems: ProblemDTO[] = [];
@@ -63,11 +61,8 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
// Filter triggers by severity
problemsList = problemsList.filter((problem) => {
if (problem.severity) {
return triggerSeverity[problem.severity].show;
} else {
return triggerSeverity[problem.priority].show;
}
const severity = problem.severity !== undefined ? Number(problem.severity) : Number(problem.priority);
return triggerSeverity[severity].show;
});
return problemsList;
@@ -97,7 +92,7 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
// Set tags if present
if (trigger.tags && trigger.tags.length === 0) {
trigger.tags = null;
trigger.tags = undefined;
}
// Handle multi-line description
@@ -109,7 +104,7 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
return trigger;
};
const parseTags = (tagStr: string) => {
const parseTags = (tagStr: string): ZBXTag[] => {
if (!tagStr) {
return [];
}
@@ -126,18 +121,18 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
};
const addTagFilter = (tag, datasource) => {
const targets = data.request.targets;
const addTagFilter = (tag: ZBXTag, datasource: DataSourceRef) => {
const targets = data.request?.targets!;
let updated = false;
for (const target of targets) {
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
const tagFilter = (target as ZabbixMetricsQuery).tags.filter;
const tagFilter = (target as ZabbixMetricsQuery).tags?.filter!;
let targetTags = parseTags(tagFilter);
const newTag = { tag: tag.tag, value: tag.value };
targetTags.push(newTag);
targetTags = _.uniqWith(targetTags, _.isEqual);
const newFilter = tagsToString(targetTags);
(target as ZabbixMetricsQuery).tags.filter = newFilter;
(target as ZabbixMetricsQuery).tags!.filter = newFilter;
updated = true;
}
}
@@ -148,18 +143,18 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
}
};
const removeTagFilter = (tag, datasource) => {
const matchTag = (t) => t.tag === tag.tag && t.value === tag.value;
const targets = data.request.targets;
const removeTagFilter = (tag: ZBXTag, datasource: DataSourceRef) => {
const matchTag = (t: ZBXTag) => t.tag === tag.tag && t.value === tag.value;
const targets = data.request?.targets!;
let updated = false;
for (const target of targets) {
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
const tagFilter = (target as ZabbixMetricsQuery).tags.filter;
const tagFilter = (target as ZabbixMetricsQuery).tags?.filter!;
let targetTags = parseTags(tagFilter);
_.remove(targetTags, matchTag);
targetTags = _.uniqWith(targetTags, _.isEqual);
const newFilter = tagsToString(targetTags);
(target as ZabbixMetricsQuery).tags.filter = newFilter;
(target as ZabbixMetricsQuery).tags!.filter = newFilter;
updated = true;
}
}
@@ -172,8 +167,8 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
const getProblemEvents = async (problem: ProblemDTO) => {
const triggerids = [problem.triggerid];
const timeFrom = Math.ceil(dateMath.parse(timeRange.from).unix());
const timeTo = Math.ceil(dateMath.parse(timeRange.to).unix());
const timeFrom = Math.ceil(dateMath.parse(timeRange.from)!.unix());
const timeTo = Math.ceil(dateMath.parse(timeRange.to)!.unix());
const ds: any = await getDataSourceSrv().get(problem.datasource);
return ds.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
};
@@ -216,11 +211,11 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
}
};
const onColumnResize = (newResized) => {
const onColumnResize = (newResized: RTResized) => {
onOptionsChange({ ...options, resizedColumns: newResized });
};
const onTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
const onTagClick = (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => {
if (ctrlKey || shiftKey) {
removeTagFilter(tag, datasource);
} else {
@@ -231,7 +226,7 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
const renderList = () => {
const problems = prepareProblems();
const fontSize = parseInt(options.fontSize.slice(0, options.fontSize.length - 1), 10);
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : undefined;
return (
<AlertList
@@ -248,7 +243,7 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
const renderTable = () => {
const problems = prepareProblems();
const fontSize = parseInt(options.fontSize.slice(0, options.fontSize.length - 1), 10);
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : undefined;
return (
<ProblemList

View File

@@ -5,7 +5,7 @@ import {
ZBX_ACK_ACTION_ACK,
ZBX_ACK_ACTION_CHANGE_SEVERITY,
ZBX_ACK_ACTION_CLOSE,
} from '../../datasource-zabbix/constants';
} from '../../datasource/constants';
import {
Button,
VerticalGroup,

View File

@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { ProblemDTO } from '../../../datasource-zabbix/types';
import { ProblemDTO } from '../../../datasource/types';
interface AlertAcknowledgesProps {
problem: ProblemDTO;

View File

@@ -1,6 +1,7 @@
import React, { PureComponent, CSSProperties } from 'react';
import classNames from 'classnames';
import { cx } from '@emotion/css';
import _ from 'lodash';
// eslint-disable-next-line
import moment from 'moment';
import { isNewProblem, formatLastChange } from '../../utils';
import { ProblemsPanelOptions, TriggerSeverity } from '../../types';
@@ -8,7 +9,7 @@ import { AckProblemData, AckModal } from '../AckModal';
import EventTag from '../EventTag';
import AlertAcknowledges from './AlertAcknowledges';
import AlertIcon from './AlertIcon';
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
import { ProblemDTO, ZBXTag } from '../../../datasource/types';
import { ModalController } from '../../../components';
import { DataSourceRef } from '@grafana/data';
import { Tooltip } from '@grafana/ui';
@@ -36,10 +37,10 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
render() {
const { problem, panelOptions } = this.props;
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', {
const cardClass = cx('alert-rule-item', 'zbx-trigger-card', {
'zbx-trigger-highlighted': panelOptions.highlightBackground,
});
const descriptionClass = classNames('alert-rule-item__text', {
const descriptionClass = cx('alert-rule-item__text', {
'zbx-description--newline': panelOptions.descriptionAtNewLine,
});
@@ -155,7 +156,7 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
<span>{lastchange || 'last change unknown'}</span>
<div className="trigger-info-block zbx-status-icons">
{problem.url && (
<a href={problem.url} target="_blank">
<a href={problem.url} target="_blank" rel="noreferrer">
<i className="fa fa-external-link"></i>
</a>
)}
@@ -239,19 +240,23 @@ const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)';
function AlertStatus(props) {
const { problem, okColor, problemColor, blink } = props;
const status = problem.value === '0' ? 'RESOLVED' : 'PROBLEM';
const color = problem.value === '0' ? okColor || DEFAULT_OK_COLOR : problemColor || DEFAULT_PROBLEM_COLOR;
const className = classNames(
const color: string = problem.value === '0' ? okColor || DEFAULT_OK_COLOR : problemColor || DEFAULT_PROBLEM_COLOR;
const className = cx(
'zbx-trigger-state',
{ 'alert-state-critical': problem.value === '1' },
{ 'alert-state-ok': problem.value === '0' },
{ 'zabbix-trigger--blinked': blink }
);
return <span className={className}>{status}</span>;
return (
<span className={className} style={{ color: color }}>
{status}
</span>
);
}
function AlertSeverity(props) {
const { severityDesc, highlightBackground, blink } = props;
const className = classNames('zbx-trigger-severity', { 'zabbix-trigger--blinked': blink });
const className = cx('zbx-trigger-severity', { 'zabbix-trigger--blinked': blink });
const style: CSSProperties = {};
if (!highlightBackground) {
style.color = severityDesc.color;

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { cx, css } from '@emotion/css';
import { GFHeartIcon } from '../../../components';
import { ProblemDTO } from '../../../datasource-zabbix/types';
import { ProblemDTO } from '../../../datasource/types';
interface Props {
problem: ProblemDTO;

View File

@@ -1,9 +1,9 @@
import React, { PureComponent, CSSProperties } from 'react';
import classNames from 'classnames';
import React, { PureComponent } from 'react';
import { cx } from '@emotion/css';
import { ProblemsPanelOptions } from '../../types';
import { AckProblemData } from '../AckModal';
import AlertCard from './AlertCard';
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
import { ProblemDTO, ZBXTag } from '../../../datasource/types';
import { DataSourceRef } from '@grafana/data';
export interface AlertListProps {
@@ -13,7 +13,7 @@ export interface AlertListProps {
pageSize?: number;
fontSize?: number;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onTagClick?: (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
interface AlertListState {
@@ -45,7 +45,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
});
};
handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
handleTagClick = (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
}
@@ -60,7 +60,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
const currentProblems = this.getCurrentProblems(this.state.page);
let fontSize = parseInt(panelOptions.fontSize.slice(0, panelOptions.fontSize.length - 1), 10);
fontSize = fontSize && fontSize !== 100 ? fontSize : null;
const alertListClass = classNames('alert-rule-list', { [`font-size--${fontSize}`]: fontSize });
const alertListClass = cx('alert-rule-list', { [`font-size--${fontSize}`]: !!fontSize });
return (
<div className="triggers-panel-container" key="alertListContainer">
@@ -117,7 +117,7 @@ class PaginationControl extends PureComponent<PaginationControlProps> {
const pageLinks = [];
for (let i = startPage; i < endPage; i++) {
const pageLinkClass = classNames('triggers-panel-page-link', 'pointer', { active: i === pageIndex });
const pageLinkClass = cx('triggers-panel-page-link', 'pointer', { active: i === pageIndex });
const value = i + 1;
const pageLinkElem = (
<li key={value.toString()}>

View File

@@ -1,7 +1,6 @@
import React, { PureComponent } from 'react';
import { DataSourceRef } from '@grafana/data';
import { Tooltip } from '@grafana/ui';
import { ZBXTag } from '../../datasource-zabbix/types';
import { ZBXTag } from '../../datasource/types';
const TAG_COLORS = [
'#E24D42',

View File

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { cx, css } from '@emotion/css';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable } from '@grafana/ui';
import { ZBXScript, APIExecuteScriptResponse } from '../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { ZBXScript, APIExecuteScriptResponse } from '../../datasource/zabbix/connectors/zabbix_api/types';
import { FAIcon } from '../../components';
interface Props extends Themeable {

View File

@@ -1,17 +1,6 @@
import React, { FormEvent } from 'react';
import {
Button,
ColorPicker,
HorizontalGroup,
InlineField,
InlineFieldRow,
InlineLabel,
InlineSwitch,
Input,
VerticalGroup,
} from '@grafana/ui';
import { StandardEditorProps } from '@grafana/data';
import { GFHeartIcon } from '../../components';
import { ColorPicker, InlineField, InlineFieldRow, InlineLabel, InlineSwitch, Input, VerticalGroup } from '@grafana/ui';
import { TriggerSeverity } from '../types';
type Props = StandardEditorProps<TriggerSeverity[]>;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { css } from '@emotion/css';
import { RTCell } from '../../types';
import { ProblemDTO } from '../../../datasource-zabbix/types';
import { ProblemDTO } from '../../../datasource/types';
import { FAIcon } from '../../../components';
import { useTheme, stylesFactory } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { ZBXAcknowledge } from '../../../datasource-zabbix/types';
import { ZBXAcknowledge } from '../../../datasource/types';
interface AcknowledgesListProps {
acknowledges: ZBXAcknowledge[];

View File

@@ -1,11 +1,12 @@
import React, { FC, PureComponent } from 'react';
// eslint-disable-next-line
import moment from 'moment';
import { TimeRange, DataSourceRef } from '@grafana/data';
import { Tooltip } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
import * as utils from '../../../datasource-zabbix/utils';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag } from '../../../datasource-zabbix/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import * as utils from '../../../datasource/utils';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag, ZBXItem } from '../../../datasource/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types';
import { AckModal, AckProblemData } from '../AckModal';
import EventTag from '../EventTag';
import AcknowledgesList from './AcknowledgesList';
@@ -13,7 +14,6 @@ import ProblemTimeline from './ProblemTimeline';
import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components';
import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal';
import ProblemStatusBar from './ProblemStatusBar';
import { ZBXItem } from '../../../datasource-zabbix/types';
import { RTRow } from '../../types';
interface ProblemDetailsProps extends RTRow<ProblemDTO> {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Tooltip } from '@grafana/ui';
import FAIcon from '../../../components/FAIcon/FAIcon';
import { ZBXAlert, ProblemDTO } from '../../../datasource-zabbix/types';
import { ZBXAlert, ProblemDTO } from '../../../datasource/types';
export interface ProblemStatusBarProps {
problem: ProblemDTO;
@@ -61,7 +61,7 @@ function ProblemStatusBarItem(props: ProblemStatusBarItemProps) {
);
}
return link ? (
<a href={link} target="_blank">
<a href={link} target="_blank" rel="noreferrer">
{item}
</a>
) : (

View File

@@ -1,7 +1,8 @@
import React, { PureComponent } from 'react';
import _ from 'lodash';
// eslint-disable-next-line
import moment from 'moment';
import { ZBXEvent, ZBXAcknowledge } from '../../../datasource-zabbix/types';
import { ZBXEvent, ZBXAcknowledge } from '../../../datasource/types';
import { TimeRange } from '@grafana/data';
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
@@ -370,7 +371,12 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
return <rect key={`${event.eventid}-${index}`} className={className} {...attributes} />;
});
return [firstItem, eventsIntervalItems];
return (
<>
{firstItem}
{eventsIntervalItems}
</>
);
}
}

View File

@@ -1,7 +1,8 @@
import React, { PureComponent } from 'react';
import { cx } from '@emotion/css';
import ReactTable from 'react-table-6';
import classNames from 'classnames';
import _ from 'lodash';
// eslint-disable-next-line
import moment from 'moment';
import { isNewProblem } from '../../utils';
import EventTag from '../EventTag';
@@ -9,8 +10,8 @@ import { ProblemDetails } from './ProblemDetails';
import { AckProblemData } from '../AckModal';
import { FAIcon, GFHeartIcon } from '../../../components';
import { ProblemsPanelOptions, RTCell, RTResized, TriggerSeverity } from '../../types';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource-zabbix/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types';
import { AckCell } from './AckCell';
import { DataSourceRef, TimeRange } from '@grafana/data';
@@ -28,7 +29,7 @@ export interface ProblemListProps {
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
onExecuteScript: (problem: ProblemDTO, scriptid: string) => Promise<APIExecuteScriptResponse>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onTagClick?: (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => void;
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
onColumnResize?: (newResized: RTResized) => void;
}
@@ -43,8 +44,9 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
rootWidth: number;
rootRef: any;
constructor(props) {
constructor(props: ProblemListProps) {
super(props);
this.rootWidth = 0;
this.state = {
expanded: {},
expandedProblems: {},
@@ -52,12 +54,12 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
};
}
setRootRef = (ref) => {
setRootRef = (ref: any) => {
this.rootRef = ref;
};
handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => {
return this.props.onProblemAck(problem, data);
return this.props.onProblemAck!(problem, data);
};
onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => {};
@@ -102,7 +104,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
});
};
handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
handleTagClick = (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
}
@@ -216,7 +218,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
const columns = this.buildColumns();
this.rootWidth = this.rootRef && this.rootRef.clientWidth;
const { pageSize, fontSize, panelOptions } = this.props;
const panelClass = classNames('panel-problems', { [`font-size--${fontSize}`]: fontSize });
const panelClass = cx('panel-problems', { [`font-size--${fontSize}`]: !!fontSize });
let pageSizeOptions = [5, 10, 20, 25, 50, 100];
if (pageSize) {
pageSizeOptions.push(pageSize);
@@ -330,7 +332,7 @@ function StatusIconCell(props: RTCell<ProblemDTO>, highlightNewerThan?: string)
if (highlightNewerThan) {
newProblem = isNewProblem(props.original, highlightNewerThan);
}
const className = classNames(
const className = cx(
'zbx-problem-status-icon',
{ 'problem-status--new': newProblem },
{ 'zbx-problem': props.value === '1' },
@@ -348,7 +350,7 @@ function GroupCell(props: RTCell<ProblemDTO>) {
}
function ProblemCell(props: RTCell<ProblemDTO>) {
const comments = props.original.comments;
// const comments = props.original.comments;
return (
<div>
<span className="problem-description">{props.value}</span>

View File

@@ -1,6 +1,6 @@
import _ from 'lodash';
import { getNextRefIdChar } from './utils';
import { ShowProblemTypes } from '../datasource-zabbix/types';
import { ShowProblemTypes } from '../datasource/types';
import { ProblemsPanelOptions } from './types';
import { PanelModel } from '@grafana/data';
@@ -71,7 +71,7 @@ export function migratePanelSchema(panel) {
}
if (schemaVersion < 7) {
const updatedTargets = [];
const updatedTargets: any[] = [];
for (const targetKey in panel.targets) {
const target = panel.targets[targetKey];
if (!isEmptyTarget(target) && !isInvalidTarget(target, targetKey)) {

View File

@@ -1,9 +1,15 @@
import { PanelPlugin, StandardEditorProps } from '@grafana/data';
import { PanelPlugin } from '@grafana/data';
import { problemsPanelChangedHandler, problemsPanelMigrationHandler } from './migrations';
import { ProblemsPanel } from './ProblemsPanel';
import { defaultPanelOptions, ProblemsPanelOptions } from './types';
import { ResetColumnsEditor } from './components/ResetColumnsEditor';
import { ProblemColorEditor } from './components/ProblemColorEditor';
import { loadPluginCss } from '@grafana/runtime';
loadPluginCss({
dark: 'plugins/alexanderzobnin-zabbix-app/styles/dark.css',
light: 'plugins/alexanderzobnin-zabbix-app/styles/light.css',
});
export const plugin = new PanelPlugin<ProblemsPanelOptions, {}>(ProblemsPanel)
.setPanelChangeHandler(problemsPanelChangedHandler)

View File

@@ -1,4 +1,3 @@
import { DataSourceRef } from '@grafana/data';
import { CURRENT_SCHEMA_VERSION } from './migrations';
export interface ProblemsPanelOptions {
@@ -26,7 +25,7 @@ export interface ProblemsPanelOptions {
showEvents?: Number[];
limit?: number;
// View options
fontSize?: string;
fontSize: string;
pageSize?: number;
problemTimeline?: boolean;
highlightBackground?: boolean;
@@ -36,9 +35,9 @@ export interface ProblemsPanelOptions {
lastChangeFormat?: string;
resizedColumns?: RTResized;
// Triggers severity and colors
triggerSeverity?: TriggerSeverity[];
okEventColor?: TriggerColor;
ackEventColor?: TriggerColor;
triggerSeverity: TriggerSeverity[];
okEventColor: TriggerColor;
ackEventColor: TriggerColor;
markAckEvents?: boolean;
}
@@ -70,7 +69,7 @@ export const defaultPanelOptions: Partial<ProblemsPanelOptions> = {
descriptionAtNewLine: false,
// Options
sortProblems: 'lastchange',
limit: null,
limit: undefined,
// View options
layout: 'table',
fontSize: '100%',

View File

@@ -1,34 +1,34 @@
import _ from 'lodash';
import moment from 'moment';
import { DataQuery } from '@grafana/data';
import * as utils from '../datasource-zabbix/utils';
import { ProblemDTO } from 'datasource-zabbix/types';
import { DataQuery, dateMath } from '@grafana/data';
import * as utils from '../datasource/utils';
import { ProblemDTO } from 'datasource/types';
export function isNewProblem(problem: ProblemDTO, highlightNewerThan: string): boolean {
try {
const highlightIntervalMs = utils.parseInterval(highlightNewerThan);
const durationSec = (Date.now() - problem.timestamp * 1000);
const durationSec = Date.now() - problem.timestamp * 1000;
return durationSec < highlightIntervalMs;
} catch (e) {
return false;
}
}
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
const DEFAULT_TIME_FORMAT = 'DD MMM YYYY HH:mm:ss';
export function formatLastChange(lastchangeUnix: number, customFormat?: string) {
const timestamp = moment.unix(lastchangeUnix);
const date = new Date(lastchangeUnix);
const timestamp = dateMath.parse(date);
const format = customFormat || DEFAULT_TIME_FORMAT;
const lastchange = timestamp.format(format);
const lastchange = timestamp!.format(format);
return lastchange;
}
export const getNextRefIdChar = (queries: DataQuery[]): string => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, refId => {
return _.every(queries, other => {
const nextLetter = _.find(letters, (refId) => {
return _.every(queries, (other) => {
return other.refId !== refId;
});
});
return nextLetter || 'A';
};

View File

@@ -576,7 +576,7 @@
font-size: 1% * $i * 10;
& .rt-tr .rt-td.custom-expander i {
font-size: 1.2rem * $i / 10;
font-size: calc(1.2rem * $i / 10);
}
.problem-details-container.show {
@@ -594,7 +594,7 @@
font-size: 1% * $i * 10;
& .rt-tr .rt-td.custom-expander i {
font-size: 1.2rem * $i / 10;
font-size: calc(1.2rem * $i / 10);
}
.problem-details-container.show {

View File

@@ -4,22 +4,9 @@
import { JSDOM } from 'jsdom';
import { PanelCtrl, MetricsPanelCtrl } from './panelStub';
// Suppress messages
console.log = () => {};
// Mock Grafana modules that are not available outside of the core project
// Required for loading module.js
jest.mock('angular', () => {
return {
module: function() {
return {
directive: function() {},
service: function() {},
factory: function() {}
};
}
};
}, {virtual: true});
jest.mock('grafana/app/features/templating/template_srv', () => {
return {};
}, {virtual: true});
@@ -33,6 +20,9 @@ jest.mock('@grafana/runtime', () => {
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue(),
}),
getTemplateSrv: () => ({
replace: jest.fn().mockImplementation(query => query),
}),
};
}, {virtual: true});
@@ -101,13 +91,8 @@ jest.mock('grafana/app/core/utils/kbn', () => {
};
}, {virtual: true});
// jest.mock('@grafana/ui', () => {
// return {};
// }, {virtual: true});
// Required for loading angularjs
let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
// Setup jsdom
let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
global.window = dom.window;
global.document = global.window.document;
global.Node = window.Node;

View File

@@ -1,25 +0,0 @@
export let templateSrvMock = {
replace: jest.fn().mockImplementation(query => query)
};
export let backendSrvMock = {
datasourceRequest: jest.fn()
};
export let datasourceSrvMock = {
loadDatasource: jest.fn(),
getAll: jest.fn()
};
export let timeSrvMock = {
timeRange: jest.fn().mockReturnValue({ from: '', to: '' })
};
const defaultExports = {
templateSrvMock,
backendSrvMock,
datasourceSrvMock,
timeSrvMock,
};
export default defaultExports;

25
src/test-setup/mocks.ts Normal file
View File

@@ -0,0 +1,25 @@
export const templateSrvMock = {
replace: jest.fn().mockImplementation((query) => query),
};
export const backendSrvMock = {
datasourceRequest: jest.fn(),
};
export const datasourceSrvMock = {
loadDatasource: jest.fn(),
getAll: jest.fn(),
};
export const timeSrvMock = {
timeRange: jest.fn().mockReturnValue({ from: '', to: '' }),
};
const defaultExports = {
templateSrvMock,
backendSrvMock,
datasourceSrvMock,
timeSrvMock,
};
export default defaultExports;