Problems count mode (#1493)

* Problems count mode

* Use tooltip from grafana ui

* Add editors for new modes

* Fix macro mode

* Fix bugs

* Unify editors to use one Triggers editor for all count queries

* Use time range toggle for triggers query, #918

* Add item tags suport for triggers count mode

* Fix triggers count by items

* Use data frames for triggers data, #1441

* Return empty result if no items found

* Add migration for problems count mode

* bump version to 4.3.0-pre

* Add zip task to makefile

* Add schema to query model

* Minor refactor

* Refactor: move components to separate files

* Minor refactor

* Support url in event tags

* Add tooltip with link url

* Update grafana packages

* Fix adding new problems panel

* ProblemDetails: rewrite as a functional component

* minor refactor
This commit is contained in:
Alexander Zobnin
2023-01-20 14:23:46 +01:00
committed by GitHub
parent 445b46a6aa
commit a5c239f77b
31 changed files with 2216 additions and 514 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,7 @@
.vscode .vscode
*.bat *.bat
.DS_Store
# Grafana linter config # Grafana linter config
# .jshintrc # .jshintrc
@@ -38,6 +39,7 @@ yarn-error.log
# Built plugin # Built plugin
dist/ dist/
ci/ ci/
alexanderzobnin-zabbix-app.zip
# Grafana toolkit configs # Grafana toolkit configs
# .prettierrc.js # .prettierrc.js

View File

@@ -82,3 +82,8 @@ sign-package:
yarn sign yarn sign
package: install dist sign-package package: install dist sign-package
zip:
cp -r dist/ alexanderzobnin-zabbix-app
zip -r alexanderzobnin-zabbix-app.zip alexanderzobnin-zabbix-app
rm -rf alexanderzobnin-zabbix-app

View File

@@ -31,14 +31,14 @@
"devDependencies": { "devDependencies": {
"@emotion/css": "11.1.3", "@emotion/css": "11.1.3",
"@emotion/react": "11.1.5", "@emotion/react": "11.1.5",
"@grafana/data": "9.1.2", "@grafana/data": "9.3.2",
"@grafana/e2e": "9.2.5", "@grafana/e2e": "9.2.5",
"@grafana/e2e-selectors": "9.2.5", "@grafana/e2e-selectors": "9.2.5",
"@grafana/eslint-config": "^5.1.0", "@grafana/eslint-config": "^5.1.0",
"@grafana/runtime": "9.1.2", "@grafana/runtime": "9.3.2",
"@grafana/toolkit": "9.1.2", "@grafana/toolkit": "9.1.2",
"@grafana/tsconfig": "^1.2.0-rc1", "@grafana/tsconfig": "^1.2.0-rc1",
"@grafana/ui": "9.1.2", "@grafana/ui": "9.3.2",
"@popperjs/core": "2.4.0", "@popperjs/core": "2.4.0",
"@swc/core": "^1.2.144", "@swc/core": "^1.2.144",
"@swc/helpers": "^0.4.12", "@swc/helpers": "^0.4.12",

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
import * as c from '../constants'; import * as c from '../constants';
import * as migrations from '../migrations'; import { migrate, DS_QUERY_SCHEMA } from '../migrations';
import { ZabbixDatasource } from '../datasource'; import { ZabbixDatasource } from '../datasource';
import { ShowProblemTypes, ZabbixDSOptions, ZabbixMetricsQuery, ZabbixQueryOptions } from '../types'; import { ShowProblemTypes, ZabbixDSOptions, ZabbixMetricsQuery, ZabbixQueryOptions } from '../types';
import { MetricsQueryEditor } from './QueryEditor/MetricsQueryEditor'; import { MetricsQueryEditor } from './QueryEditor/MetricsQueryEditor';
@@ -13,6 +13,7 @@ import { ProblemsQueryEditor } from './QueryEditor/ProblemsQueryEditor';
import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor'; import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor';
import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor'; import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor'; import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor';
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [ const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
{ {
@@ -38,29 +39,32 @@ const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
{ {
value: c.MODE_TRIGGERS, value: c.MODE_TRIGGERS,
label: 'Triggers', label: 'Triggers',
description: 'Query triggers data', description: 'Count triggers',
}, },
{ {
value: c.MODE_PROBLEMS, value: c.MODE_PROBLEMS,
label: 'Problems', label: 'Problems',
description: 'Query problems', description: 'Query problems',
}, },
{
value: c.MODE_MACROS,
label: 'User macros',
description: 'User Macros',
},
]; ];
const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({ const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
schema: DS_QUERY_SCHEMA,
queryType: c.MODE_METRICS, queryType: c.MODE_METRICS,
group: { filter: '' }, group: { filter: '' },
host: { filter: '' }, host: { filter: '' },
application: { filter: '' }, application: { filter: '' },
itemTag: { filter: '' }, itemTag: { filter: '' },
item: { filter: '' }, item: { filter: '' },
macro: { filter: '' },
functions: [], functions: [],
triggers: {
count: true,
minSeverity: 3,
acknowledged: 2,
},
trigger: { filter: '' }, trigger: { filter: '' },
countTriggersBy: '',
tags: { filter: '' }, tags: { filter: '' },
proxy: { filter: '' }, proxy: { filter: '' },
textFilter: '', textFilter: '',
@@ -70,6 +74,7 @@ const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
disableDataAlignment: false, disableDataAlignment: false,
useZabbixValueMapping: false, useZabbixValueMapping: false,
useTrends: 'default', useTrends: 'default',
count: false,
}, },
table: { table: {
skipEmptyValues: false, skipEmptyValues: false,
@@ -96,6 +101,7 @@ function getProblemsQueryDefaults(): Partial<ZabbixMetricsQuery> {
hostProxy: false, hostProxy: false,
limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT, limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT,
useTimeRange: false, useTimeRange: false,
count: false,
}, },
}; };
} }
@@ -119,7 +125,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
// Migrate query on load // Migrate query on load
useEffect(() => { useEffect(() => {
const migratedQuery = migrations.migrate(query); const migratedQuery = migrate(query);
onChange(migratedQuery); onChange(migratedQuery);
}, []); }, []);
@@ -184,6 +190,10 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
return <TriggersQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />; return <TriggersQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
}; };
const renderUserMacrosEditor = () => {
return <UserMacrosQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
};
return ( return (
<> <>
<InlineFieldRow> <InlineFieldRow>
@@ -206,6 +216,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
{queryType === c.MODE_ITSERVICE && renderITServicesEditor()} {queryType === c.MODE_ITSERVICE && renderITServicesEditor()}
{queryType === c.MODE_PROBLEMS && renderProblemsEditor()} {queryType === c.MODE_PROBLEMS && renderProblemsEditor()}
{queryType === c.MODE_TRIGGERS && renderTriggersEditor()} {queryType === c.MODE_TRIGGERS && renderTriggersEditor()}
{queryType === c.MODE_MACROS && renderUserMacrosEditor()}
<QueryOptionsEditor queryType={queryType} queryOptions={query.options} onChange={onOptionsChange} /> <QueryOptionsEditor queryType={queryType} queryOptions={query.options} onChange={onOptionsChange} />
</> </>
); );

View File

@@ -201,6 +201,12 @@ export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props)
onChange={onPropChange('acknowledged')} onChange={onPropChange('acknowledged')}
/> />
</InlineField> </InlineField>
<InlineField label="Use time range" labelWidth={24}>
<InlineSwitch
value={queryOptions.useTimeRange}
onChange={() => onChange({ ...queryOptions, useTimeRange: !queryOptions.useTimeRange })}
/>
</InlineField>
</> </>
); );
}; };

View File

@@ -1,14 +1,21 @@
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use'; import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Select } from '@grafana/ui'; import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow'; import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components'; import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils'; import { getVariableOptions } from './utils';
import { itemTagToString } from '../../utils';
import { ZabbixDatasource } from '../../datasource'; import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types'; import { ZabbixMetricsQuery, ZBXItem, ZBXItemTag } from '../../types';
const countByOptions: Array<SelectableValue<string>> = [
{ value: '', label: 'All triggers' },
{ value: 'problems', label: 'Problems' },
{ value: 'items', label: 'Items' },
];
const severityOptions: Array<SelectableValue<number>> = [ const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' }, { value: 0, label: 'Not classified' },
@@ -77,9 +84,80 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
return options; return options;
}, [query.group.filter, query.host.filter]); }, [query.group.filter, query.host.filter]);
const loadTagOptions = async (group: string, host: string) => {
if (!datasource.zabbix.isZabbix54OrHigher()) {
return [];
}
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, null, null, {});
const tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => item.tags || []));
const tagList = _.uniqBy(tags, (t) => t.tag + t.value || '').map((t) => itemTagToString(t));
let options: Array<SelectableValue<string>> = tagList?.map((tag) => ({
value: tag,
label: tag,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: tagsLoading, value: tagOptions }, fetchItemTags] = useAsyncFn(async () => {
const options = await loadTagOptions(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;
}, []);
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: Array<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 // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();
@@ -93,6 +171,27 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
fetchApps(); fetchApps();
}, [groupFilter, hostFilter]); }, [groupFilter, hostFilter]);
useEffect(() => {
fetchItemTags();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchProxies();
}, []);
useEffect(() => {
fetchItems();
}, [groupFilter, hostFilter, appFilter, tagFilter]);
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) => { const onFilterChange = (prop: string) => {
return (value: string) => { return (value: string) => {
if (value !== null) { if (value !== null) {
@@ -107,8 +206,27 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
} }
}; };
const onCountByChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, countTriggersBy: option.value! });
}
};
const supportsApplications = datasource.zabbix.supportsApplications();
return ( return (
<> <>
<QueryEditorRow>
<InlineField label="Count by" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.countTriggersBy}
options={countByOptions}
onChange={onCountByChange}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow> <QueryEditorRow>
<InlineField label="Group" labelWidth={12}> <InlineField label="Group" labelWidth={12}>
<MetricPicker <MetricPicker
@@ -128,8 +246,20 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
onChange={onFilterChange('host')} onChange={onFilterChange('host')}
/> />
</InlineField> </InlineField>
{query.countTriggersBy === 'problems' && (
<InlineField label="Proxy" labelWidth={12}>
<MetricPicker
width={24}
value={query.proxy?.filter}
options={proxiesOptions}
isLoading={proxiesLoading}
onChange={onFilterChange('proxy')}
/>
</InlineField>
)}
</QueryEditorRow> </QueryEditorRow>
<QueryEditorRow> <QueryEditorRow>
{(supportsApplications || query.countTriggersBy !== 'items') && (
<InlineField label="Application" labelWidth={12}> <InlineField label="Application" labelWidth={12}>
<MetricPicker <MetricPicker
width={24} width={24}
@@ -139,19 +269,64 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
onChange={onFilterChange('application')} onChange={onFilterChange('application')}
/> />
</InlineField> </InlineField>
)}
{!supportsApplications && query.countTriggersBy === 'items' && (
<InlineField label="Item tag" labelWidth={12}>
<MetricPicker
width={24}
value={query.itemTag.filter}
options={tagOptions}
isLoading={tagsLoading}
onChange={onFilterChange('itemTag')}
/>
</InlineField>
)}
{query.countTriggersBy === 'problems' && (
<>
<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>
</>
)}
{query.countTriggersBy === 'items' && (
<InlineField label="Item" labelWidth={12}>
<MetricPicker
width={24}
value={query.item.filter}
options={itemOptions}
isLoading={itemsLoading}
onChange={onFilterChange('item')}
/>
</InlineField>
)}
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Min severity" labelWidth={12}> <InlineField label="Min severity" labelWidth={12}>
<Select <Select
isSearchable={false} isSearchable={false}
width={24} width={24}
value={query.triggers?.minSeverity} value={query.options?.minSeverity}
options={severityOptions} options={severityOptions}
onChange={onMinSeverityChange} onChange={onMinSeverityChange}
/> />
</InlineField> </InlineField>
<InlineField label="Count" labelWidth={12}> <InlineField label="Count" labelWidth={12}>
<InlineSwitch <InlineSwitch
value={query.triggers?.count} value={query.options?.count}
onChange={() => onChange({ ...query, triggers: { ...query.triggers, count: !query.triggers?.count } })} onChange={() => onChange({ ...query, options: { ...query.options, count: !query.options?.count } })}
/> />
</InlineField> </InlineField>
</QueryEditorRow> </QueryEditorRow>

View File

@@ -0,0 +1,131 @@
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 UserMacrosQueryEditor = ({ 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: Array<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 loadMacrosOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const macros = await datasource.zabbix.getAllMacros(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = macros?.map((m) => ({
value: m.macro,
label: m.macro,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: macrosLoading, value: macrosOptions }, fetchmacros] = useAsyncFn(async () => {
const options = await loadMacrosOptions(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(() => {
fetchmacros();
}, [groupFilter, hostFilter]);
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="Macros" labelWidth={12}>
<MetricPicker
width={24}
value={query.macro.filter}
options={macrosOptions}
isLoading={macrosLoading}
onChange={onFilterChange('macro')}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -13,6 +13,7 @@ export const MODE_TEXT = '2';
export const MODE_ITEMID = '3'; export const MODE_ITEMID = '3';
export const MODE_TRIGGERS = '4'; export const MODE_TRIGGERS = '4';
export const MODE_PROBLEMS = '5'; export const MODE_PROBLEMS = '5';
export const MODE_MACROS = '6';
// Triggers severity // Triggers severity
export const SEV_NOT_CLASSIFIED = 0; export const SEV_NOT_CLASSIFIED = 0;

View File

@@ -241,10 +241,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return this.queryITServiceData(target, timeRange, request); return this.queryITServiceData(target, timeRange, request);
} else if (target.queryType === c.MODE_TRIGGERS) { } else if (target.queryType === c.MODE_TRIGGERS) {
// Triggers query // Triggers query
return this.queryTriggersData(target, timeRange); return this.queryTriggersData(target, timeRange, request);
} else if (target.queryType === c.MODE_PROBLEMS) { } else if (target.queryType === c.MODE_PROBLEMS) {
// Problems query // Problems query
return this.queryProblems(target, timeRange, request); return this.queryProblems(target, timeRange, request);
} else if (target.queryType === c.MODE_MACROS) {
// UserMacro query
return this.queryUserMacrosData(target);
} else { } else {
return []; return [];
} }
@@ -254,7 +257,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return Promise.all(_.flatten(promises)) return Promise.all(_.flatten(promises))
.then(_.flatten) .then(_.flatten)
.then((data) => { .then((data) => {
if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) { if (
data &&
data.length > 0 &&
isDataFrame(data[0]) &&
!utils.isProblemsDataFrame(data[0]) &&
!utils.isMacrosDataFrame(data[0]) &&
!utils.nonTimeSeriesDataFrame(data[0])
) {
data = responseHandler.alignFrames(data); data = responseHandler.alignFrames(data);
if (responseHandler.isConvertibleToWide(data)) { if (responseHandler.isConvertibleToWide(data)) {
console.log('Converting response to the wide format'); console.log('Converting response to the wide format');
@@ -504,31 +514,120 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return this.handleBackendPostProcessingResponse(processedResponse, request, target); return this.handleBackendPostProcessingResponse(processedResponse, request, target);
} }
queryTriggersData(target, timeRange) { async queryUserMacrosData(target) {
const [timeFrom, timeTo] = timeRange;
return this.zabbix.getHostsFromTarget(target).then((results) => {
const [hosts, apps] = results;
if (hosts.length) {
const hostids = _.map(hosts, 'hostid');
const appids = _.map(apps, 'applicationid');
const options = {
minSeverity: target.triggers.minSeverity,
acknowledged: target.triggers.acknowledged,
count: target.triggers.count,
timeFrom: timeFrom,
timeTo: timeTo,
};
const groupFilter = target.group.filter; const groupFilter = target.group.filter;
return Promise.all([ const hostFilter = target.host.filter;
this.zabbix.getHostAlerts(hostids, appids, options), const macroFilter = target.macro.filter;
this.zabbix.getGroups(groupFilter), const macros = await this.zabbix.getUMacros(groupFilter, hostFilter, macroFilter);
]).then(([triggers, groups]) => { const hostmacroids = _.map(macros, 'hostmacroid');
return responseHandler.handleTriggersResponse(triggers, groups, timeRange); const userMacros = await this.zabbix.getUserMacros(hostmacroids);
}); return responseHandler.handleMacro(userMacros, target);
} else { }
async queryTriggersData(target: ZabbixMetricsQuery, timeRange, request) {
if (target.countTriggersBy === 'items') {
return this.queryTriggersICData(target, timeRange);
} else if (target.countTriggersBy === 'problems') {
return this.queryTriggersPCData(target, timeRange, request);
}
const [hosts, apps] = await this.zabbix.getHostsApsFromTarget(target);
if (!hosts.length) {
return Promise.resolve([]); return Promise.resolve([]);
} }
const groupFilter = target.group.filter;
const groups = await this.zabbix.getGroups(groupFilter);
const hostids = hosts?.map((h) => h.hostid);
const appids = apps?.map((a) => a.applicationid);
const options = getTriggersOptions(target, timeRange);
const alerts = await this.zabbix.getHostAlerts(hostids, appids, options);
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
}
async queryTriggersICData(target, timeRange) {
const getItemOptions = { itemtype: 'num' };
const [hosts, apps, items] = await this.zabbix.getHostsFromICTarget(target, getItemOptions);
if (!hosts.length) {
return Promise.resolve([]);
}
const groupFilter = target.group.filter;
const groups = await this.zabbix.getGroups(groupFilter);
const hostids = hosts?.map((h) => h.hostid);
const appids = apps?.map((a) => a.applicationid);
const itemids = items?.map((i) => i.itemid);
if (!itemids.length) {
return Promise.resolve([]);
}
const options = getTriggersOptions(target, timeRange);
const alerts = await this.zabbix.getHostICAlerts(hostids, appids, itemids, options);
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
}
async queryTriggersPCData(target: ZabbixMetricsQuery, timeRange, request) {
const [timeFrom, timeTo] = timeRange;
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, request.scopedVars);
// replaceTemplateVars() builds regex-like string, so we should trim it.
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', '');
const tags = utils.parseTags(tagsFilterStr);
tags.forEach((tag) => {
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
tag.operator = 1;
}); });
const problemsOptions: any = {
minSeverity: target.options?.minSeverity,
limit: target.options?.limit,
};
if (tags && tags.length) {
problemsOptions.tags = tags;
}
if (target.options?.acknowledged === 0 || target.options?.acknowledged === 1) {
problemsOptions.acknowledged = target.options?.acknowledged ? true : false;
}
if (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));
}
problemsOptions.severities = severities;
}
if (target.options.useTimeRange) {
problemsOptions.timeFrom = timeFrom;
problemsOptions.timeTo = timeTo;
}
const [hosts, apps, triggers] = await this.zabbix.getHostsFromPCTarget(target, problemsOptions);
if (!hosts.length) {
return Promise.resolve([]);
}
const groupFilter = target.group.filter;
const groups = await this.zabbix.getGroups(groupFilter);
const hostids = hosts?.map((h) => h.hostid);
const appids = apps?.map((a) => a.applicationid);
const triggerids = triggers.map((t) => t.triggerid);
const options: any = {
minSeverity: target.options?.minSeverity,
acknowledged: target.options?.acknowledged,
count: target.options.count,
};
if (target.options.useTimeRange) {
options.timeFrom = timeFrom;
options.timeTo = timeTo;
}
const alerts = await this.zabbix.getHostPCAlerts(hostids, appids, triggerids, options);
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
} }
queryProblems(target: ZabbixMetricsQuery, timeRange, options) { queryProblems(target: ZabbixMetricsQuery, timeRange, options) {
@@ -742,6 +841,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
templateSrv.variableExists(target.application?.filter) || templateSrv.variableExists(target.application?.filter) ||
templateSrv.variableExists(target.itemTag?.filter) || templateSrv.variableExists(target.itemTag?.filter) ||
templateSrv.variableExists(target.item?.filter) || templateSrv.variableExists(target.item?.filter) ||
templateSrv.variableExists(target.macro?.filter) ||
templateSrv.variableExists(target.proxy?.filter) || templateSrv.variableExists(target.proxy?.filter) ||
templateSrv.variableExists(target.trigger?.filter) || templateSrv.variableExists(target.trigger?.filter) ||
templateSrv.variableExists(target.textFilter) || templateSrv.variableExists(target.textFilter) ||
@@ -972,3 +1072,17 @@ function getRequestTarget(request: DataQueryRequest<any>, refId: string): any {
} }
return null; return null;
} }
const getTriggersOptions = (target: ZabbixMetricsQuery, timeRange) => {
const [timeFrom, timeTo] = timeRange;
const options: any = {
minSeverity: target.options?.minSeverity,
acknowledged: target.options?.acknowledged,
count: target.options?.count,
};
if (target.options?.useTimeRange) {
options.timeFrom = timeFrom;
options.timeTo = timeTo;
}
return options;
};

View File

@@ -2,6 +2,9 @@ import _ from 'lodash';
import { ZabbixMetricsQuery } from './types'; import { ZabbixMetricsQuery } from './types';
import * as c from './constants'; import * as c from './constants';
export const DS_QUERY_SCHEMA = 11;
export const DS_CONFIG_SCHEMA = 3;
/** /**
* Query format migration. * Query format migration.
* This module can detect query format version and make migration. * This module can detect query format version and make migration.
@@ -28,6 +31,7 @@ export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
target.host.filter = target.host.name === '*' ? convertToRegex(target.hostFilter) : target.host.name; target.host.filter = target.host.name === '*' ? convertToRegex(target.hostFilter) : target.host.name;
target.application.filter = target.application.name === '*' ? '' : target.application.name; target.application.filter = target.application.name === '*' ? '' : target.application.name;
target.item.filter = target.item.name === 'All' ? convertToRegex(target.itemFilter) : target.item.name; target.item.filter = target.item.name === 'All' ? convertToRegex(target.itemFilter) : target.item.name;
target.macro.filter = target.macro.macro === '*' ? convertToRegex(target.macroFilter) : target.macro.macro;
return target; return target;
} }
@@ -85,6 +89,32 @@ function migrateSLAProperty(target) {
} }
} }
function migrateTriggersMode(target: any) {
if (target.triggers?.minSeverity) {
target.options.minSeverity = target.triggers?.minSeverity;
delete target.triggers.minSeverity;
}
if (target.triggers?.count) {
target.options.count = target.triggers?.count;
delete target.triggers.count;
}
}
function migrateNewTriggersCountModes(target: any) {
if (target.schema >= 11) {
return;
}
if (target.queryType === '6') {
target.queryType = c.MODE_TRIGGERS;
target.countTriggersBy = 'items';
} else if (target.queryType === '7') {
target.queryType = c.MODE_TRIGGERS;
target.countTriggersBy = 'problems';
} else if (target.queryType === '8') {
target.queryType = c.MODE_MACROS;
}
}
export function migrate(target) { export function migrate(target) {
target.resultFormat = target.resultFormat || 'time_series'; target.resultFormat = target.resultFormat || 'time_series';
target = fixTargetGroup(target); target = fixTargetGroup(target);
@@ -97,6 +127,10 @@ export function migrate(target) {
migrateProblemSort(target); migrateProblemSort(target);
migrateApplications(target); migrateApplications(target);
migrateSLAProperty(target); migrateSLAProperty(target);
migrateTriggersMode(target);
migrateNewTriggersCountModes(target);
target.schema = DS_QUERY_SCHEMA;
return target; return target;
} }
@@ -115,8 +149,6 @@ function convertToRegex(str) {
} }
} }
export const DS_CONFIG_SCHEMA = 3;
export function migrateDSConfig(jsonData) { export function migrateDSConfig(jsonData) {
if (!jsonData) { if (!jsonData) {
jsonData = {}; jsonData = {};

View File

@@ -16,7 +16,7 @@ import {
TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_TIME_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME,
} from '@grafana/data'; } from '@grafana/data';
import { ZabbixMetricsQuery } from './types'; import { ZabbixMetricsQuery, ZBXGroup, ZBXTrigger } from './types';
/** /**
* Convert Zabbix API history.get response to Grafana format * Convert Zabbix API history.get response to Grafana format
@@ -74,6 +74,29 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
}); });
} }
function handleMacro(macros, target): MutableDataFrame {
const frame = new MutableDataFrame({
refId: target.refId,
name: 'macros',
fields: [
{ name: 'Host', type: FieldType.string },
{ name: 'Macros', type: FieldType.string },
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.string },
],
});
for (let i = 0; i < macros.length; i++) {
const m = macros[i];
const dataRow: any = {
Host: m.hosts[0]!.name,
Macros: m.macro,
[TIME_SERIES_VALUE_FIELD_NAME]: m.value,
};
frame.add(dataRow);
}
return frame;
}
export function seriesToDataFrame( export function seriesToDataFrame(
timeseries, timeseries,
target: ZabbixMetricsQuery, target: ZabbixMetricsQuery,
@@ -589,7 +612,7 @@ export function handleSLAResponse(itservice, slaProperty, slaObject) {
} }
} }
function handleTriggersResponse(triggers, groups, timeRange) { function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], timeRange: number[], target) {
if (!_.isArray(triggers)) { if (!_.isArray(triggers)) {
let triggersCount = null; let triggersCount = null;
try { try {
@@ -597,29 +620,47 @@ function handleTriggersResponse(triggers, groups, timeRange) {
} catch (err) { } catch (err) {
console.log('Error when handling triggers count: ', err); console.log('Error when handling triggers count: ', err);
} }
return {
target: 'triggers count', const frame = new MutableDataFrame({
datapoints: [[triggersCount, timeRange[1] * 1000]], refId: target.refId,
}; fields: [
{ name: TIME_SERIES_TIME_FIELD_NAME, type: FieldType.time, values: new ArrayVector([timeRange[1] * 1000]) },
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.number, values: new ArrayVector([triggersCount]) },
],
length: 1,
});
return frame;
} else { } else {
const stats = getTriggerStats(triggers); const stats = getTriggerStats(triggers);
const groupNames = _.map(groups, 'name'); const frame = new MutableDataFrame({
const table: any = new TableModel(); refId: target.refId,
table.addColumn({ text: 'Host group' }); fields: [{ name: 'Host group', type: FieldType.string, values: new ArrayVector() }],
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => { });
table.addColumn({ text: severity.text });
for (let i = c.TRIGGER_SEVERITY.length - 1; i >= 0; i--) {
frame.fields.push({
name: c.TRIGGER_SEVERITY[i].text,
type: FieldType.number,
config: { unit: 'none', decimals: 0 },
values: new ArrayVector(),
}); });
_.each(stats, (severity_stats, group) => {
if (_.includes(groupNames, group)) {
let row = _.map(
_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']),
(s) => s[1]
);
row = _.concat([group], ...row);
table.rows.push(row);
} }
const groupNames = groups?.map((g) => g.name);
groupNames?.forEach((group) => {
frame.add({
'Host group': group,
Disaster: stats[group] ? stats[group][5] : 0,
High: stats[group] ? stats[group][4] : 0,
Average: stats[group] ? stats[group][3] : 0,
Warning: stats[group] ? stats[group][2] : 0,
Information: stats[group] ? stats[group][1] : 0,
'Not classified': stats[group] ? stats[group][0] : 0,
}); });
return table; });
return frame;
} }
} }
@@ -673,6 +714,7 @@ export default {
convertHistory, convertHistory,
handleTrends, handleTrends,
handleText, handleText,
handleMacro,
handleHistoryAsTable, handleHistoryAsTable,
handleSLAResponse, handleSLAResponse,
handleTriggersResponse, handleTriggersResponse,

View File

@@ -34,17 +34,19 @@ export interface ZabbixConnectionTestQuery {
} }
export interface ZabbixMetricsQuery extends DataQuery { export interface ZabbixMetricsQuery extends DataQuery {
schema: number;
queryType: string; queryType: string;
datasourceId?: number; datasourceId: number;
group?: { filter: string; name?: string }; group: { filter: string; name?: string };
host?: { filter: string; name?: string }; host: { filter: string; name?: string };
application?: { filter: string; name?: string }; application: { filter: string; name?: string };
itemTag?: { filter: string; name?: string }; itemTag: { filter: string; name?: string };
item?: { filter: string; name?: string }; item: { filter: string; name?: string };
textFilter?: string; macro: { filter: string; macro?: string };
mode?: number; textFilter: string;
itemids?: string; mode: number;
useCaptureGroups?: boolean; itemids: string;
useCaptureGroups: boolean;
proxy?: { filter: string }; proxy?: { filter: string };
trigger?: { filter: string }; trigger?: { filter: string };
itServiceFilter?: string; itServiceFilter?: string;
@@ -53,6 +55,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
slaInterval?: string; slaInterval?: string;
tags?: { filter: string }; tags?: { filter: string };
triggers?: { minSeverity: number; acknowledged: number; count: boolean }; triggers?: { minSeverity: number; acknowledged: number; count: boolean };
countTriggersBy?: 'problems' | 'items' | '';
functions?: MetricFunc[]; functions?: MetricFunc[];
options?: ZabbixQueryOptions; options?: ZabbixQueryOptions;
// Problems // Problems
@@ -60,6 +63,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
// Deprecated // Deprecated
hostFilter?: string; hostFilter?: string;
itemFilter?: string; itemFilter?: string;
macroFilter?: string;
} }
export interface ZabbixQueryOptions { export interface ZabbixQueryOptions {
@@ -77,6 +81,7 @@ export interface ZabbixQueryOptions {
limit?: number; limit?: number;
useTimeRange?: boolean; useTimeRange?: boolean;
severities?: number[]; severities?: number[];
count?: boolean;
// Annotations // Annotations
showOkEvents?: boolean; showOkEvents?: boolean;
@@ -186,6 +191,7 @@ export interface VariableQuery {
application?: string; application?: string;
itemTag?: string; itemTag?: string;
item?: string; item?: string;
macro?: string;
} }
export type LegacyVariableQuery = VariableQuery | string; export type LegacyVariableQuery = VariableQuery | string;
@@ -194,6 +200,7 @@ export enum VariableQueryTypes {
Group = 'group', Group = 'group',
Host = 'host', Host = 'host',
Application = 'application', Application = 'application',
Macro = 'macro',
ItemTag = 'itemTag', ItemTag = 'itemTag',
Item = 'item', Item = 'item',
ItemValues = 'itemValues', ItemValues = 'itemValues',
@@ -330,6 +337,7 @@ export interface ZBXHost {
maintenance_status?: string; maintenance_status?: string;
proxy_hostid?: string; proxy_hostid?: string;
proxy?: any; proxy?: any;
description?: string;
} }
export interface ZBXItem { export interface ZBXItem {
@@ -340,6 +348,13 @@ export interface ZBXItem {
tags?: ZBXItemTag[]; tags?: ZBXItemTag[];
} }
export interface ZBXApp {
applicationid: string;
hostid: string;
name: string;
templateids?: string;
}
export interface ZBXItemTag { export interface ZBXItemTag {
tag: string; tag: string;
value?: string; value?: string;

View File

@@ -3,7 +3,15 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as c from './constants'; import * as c from './constants';
import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types'; import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types';
import { DataFrame, FieldType, getValueFormats, MappingType, rangeUtil, ValueMapping } from '@grafana/data'; import {
DataFrame,
FieldType,
getValueFormats,
MappingType,
rangeUtil,
TIME_SERIES_TIME_FIELD_NAME,
ValueMapping,
} from '@grafana/data';
/* /*
* This regex matches 3 types of variable reference with an optional format specifier * This regex matches 3 types of variable reference with an optional format specifier
@@ -507,6 +515,14 @@ export function isProblemsDataFrame(data: DataFrame): boolean {
); );
} }
export function isMacrosDataFrame(data: DataFrame): boolean {
return data.name === 'macros';
}
export function nonTimeSeriesDataFrame(data: DataFrame): boolean {
return !data.fields.find((f) => f.type === FieldType.time || f.name === TIME_SERIES_TIME_FIELD_NAME);
}
// Swap n and k elements. // Swap n and k elements.
export function swap<T>(list: T[], n: number, k: number): 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) { if (list === null || list.length < 2 || k > list.length - 1 || k < 0 || n > list.length - 1 || n < 0) {

View File

@@ -3,7 +3,7 @@ import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn'; import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils'; import * as utils from '../../../utils';
import { MIN_SLA_INTERVAL, ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_NONE } from '../../../constants'; import { MIN_SLA_INTERVAL, ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_NONE } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types'; import { ShowProblemTypes, ZBXProblem, ZBXTrigger } from '../../../types';
import { APIExecuteScriptResponse, JSONRPCError, ZBXScript } from './types'; import { APIExecuteScriptResponse, JSONRPCError, ZBXScript } from './types';
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
import { rangeUtil } from '@grafana/data'; import { rangeUtil } from '@grafana/data';
@@ -228,6 +228,15 @@ export class ZabbixAPIConnector {
return this.request('usermacro.get', params); return this.request('usermacro.get', params);
} }
getUserMacros(hostmacroids) {
const params = {
output: 'extend',
hostmacroids: hostmacroids,
selectHosts: ['hostid', 'name'],
};
return this.request('usermacro.get', params);
}
getGlobalMacros() { getGlobalMacros() {
const params = { const params = {
output: 'extend', output: 'extend',
@@ -506,10 +515,11 @@ export class ZabbixAPIConnector {
expandDescription: true, expandDescription: true,
expandData: true, expandData: true,
expandComment: true, expandComment: true,
expandExpression: true,
monitored: true, monitored: true,
skipDependent: true, skipDependent: true,
selectGroups: ['name', 'groupid'], selectGroups: ['name', 'groupid'],
selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid'], selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid', 'description'],
selectItems: ['itemid', 'name', 'key_', 'lastvalue'], selectItems: ['itemid', 'name', 'key_', 'lastvalue'],
// selectLastEvent: 'extend', // selectLastEvent: 'extend',
// selectTags: 'extend', // selectTags: 'extend',
@@ -680,7 +690,7 @@ export class ZabbixAPIConnector {
return this.request('trigger.get', params); return this.request('trigger.get', params);
} }
getHostAlerts(hostids, applicationids, options) { async getHostAlerts(hostids, applicationids, options): Promise<ZBXTrigger[]> {
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options; const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
const params: any = { const params: any = {
output: 'extend', output: 'extend',
@@ -697,6 +707,100 @@ export class ZabbixAPIConnector {
selectHosts: ['hostid', 'host', 'name'], selectHosts: ['hostid', 'host', 'name'],
}; };
if (count && acknowledged !== 1) {
params.countOutput = true;
if (acknowledged === 0) {
params.withLastEventUnacknowledged = true;
}
}
if (applicationids && applicationids.length) {
params.applicationids = applicationids;
}
if (timeFrom || timeTo) {
params.lastChangeSince = timeFrom;
params.lastChangeTill = timeTo;
}
let triggers = await this.request('trigger.get', params);
if (!count || acknowledged === 1) {
triggers = filterTriggersByAcknowledge(triggers, acknowledged);
if (count) {
triggers = triggers.length;
}
}
return triggers;
}
getHostICAlerts(hostids, applicationids, itemids, options) {
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
const params: any = {
output: 'extend',
hostids: hostids,
min_severity: minSeverity,
filter: { value: 1 },
expandDescription: true,
expandData: true,
expandComment: true,
monitored: true,
skipDependent: true,
selectLastEvent: 'extend',
selectGroups: 'extend',
selectHosts: ['host', 'name'],
selectItems: ['name', 'key_'],
};
if (count && acknowledged !== 1) {
params.countOutput = true;
if (acknowledged === 0) {
params.withLastEventUnacknowledged = true;
}
}
if (applicationids && applicationids.length) {
params.applicationids = applicationids;
}
if (itemids && itemids.length) {
params.itemids = itemids;
}
if (timeFrom || timeTo) {
params.lastChangeSince = timeFrom;
params.lastChangeTill = timeTo;
}
return this.request('trigger.get', params).then((triggers) => {
if (!count || acknowledged === 1) {
triggers = filterTriggersByAcknowledge(triggers, acknowledged);
if (count) {
triggers = triggers.length;
}
}
return triggers;
});
}
getHostPCAlerts(hostids, applicationids, triggerids, options) {
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
const params: any = {
output: 'extend',
hostids: hostids,
triggerids: triggerids,
min_severity: minSeverity,
filter: { value: 1 },
expandDescription: true,
expandData: true,
expandComment: true,
monitored: true,
skipDependent: true,
selectLastEvent: 'extend',
selectGroups: 'extend',
selectHosts: ['host', 'name'],
selectItems: ['name', 'key_'],
};
if (count && acknowledged !== 0 && acknowledged !== 1) { if (count && acknowledged !== 0 && acknowledged !== 1) {
params.countOutput = true; params.countOutput = true;
} }

View File

@@ -5,18 +5,22 @@ export interface ZabbixConnector {
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>; getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>; getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
getHostAlerts: (hostids, applicationids, options?) => Promise<any>; getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
getHostICAlerts: (hostids, applicationids, itemids, options?) => Promise<any>;
getHostPCAlerts: (hostids, applicationids, triggerids, options?) => Promise<any>;
getAcknowledges: (eventids) => Promise<any>; getAcknowledges: (eventids) => Promise<any>;
getITService: (serviceids?) => Promise<any>; getITService: (serviceids?) => Promise<any>;
acknowledgeEvent: (eventid, message) => Promise<any>; acknowledgeEvent: (eventid, message) => Promise<any>;
getProxies: () => Promise<any>; getProxies: () => Promise<any>;
getEventAlerts: (eventids) => Promise<any>; getEventAlerts: (eventids) => Promise<any>;
getExtendedEventData: (eventids) => Promise<any>; getExtendedEventData: (eventids) => Promise<any>;
getUserMacros: (hostmacroids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>; getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
getGroups: (groupFilter?) => any; getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any; getHosts: (groupFilter?, hostFilter?) => any;
getApps: (groupFilter?, hostFilter?, appFilter?) => any; getApps: (groupFilter?, hostFilter?, appFilter?) => any;
getUMacros: (groupFilter?, hostFilter?, macroFilter?) => any;
getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options?) => any; getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options?) => any;
getSLA: (itservices, timeRange, target, options?) => any; getSLA: (itservices, timeRange, target, options?) => any;

View File

@@ -11,7 +11,7 @@ import { SQLConnector } from './connectors/sql/sqlConnector';
import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector'; import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector';
import { ZabbixConnector } from './types'; import { ZabbixConnector } from './types';
import { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler'; import { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler';
import { ProblemDTO, ZabbixMetricsQuery, ZBXItem, ZBXItemTag } from '../types'; import { ProblemDTO, ZBXApp, ZBXHost, ZBXItem, ZBXItemTag, ZBXTrigger, ZabbixMetricsQuery } from '../types';
interface AppsResponse extends Array<any> { interface AppsResponse extends Array<any> {
appFilterEmpty?: boolean; appFilterEmpty?: boolean;
@@ -26,13 +26,18 @@ const REQUESTS_TO_PROXYFY = [
'getApps', 'getApps',
'getItems', 'getItems',
'getMacros', 'getMacros',
'getUMacros',
'getItemsByIDs', 'getItemsByIDs',
'getEvents', 'getEvents',
'getAlerts', 'getAlerts',
'getHostAlerts', 'getHostAlerts',
'getUserMacros',
'getHostICAlerts',
'getHostPCAlerts',
'getAcknowledges', 'getAcknowledges',
'getITService', 'getITService',
'getSLA', 'getSLA',
'getVersion',
'getProxies', 'getProxies',
'getEventAlerts', 'getEventAlerts',
'getExtendedEventData', 'getExtendedEventData',
@@ -50,6 +55,7 @@ const REQUESTS_TO_CACHE = [
'getApps', 'getApps',
'getItems', 'getItems',
'getMacros', 'getMacros',
'getUMacros',
'getItemsByIDs', 'getItemsByIDs',
'getITService', 'getITService',
'getProxies', 'getProxies',
@@ -65,6 +71,9 @@ const REQUESTS_TO_BIND = [
'getEvents', 'getEvents',
'getAlerts', 'getAlerts',
'getHostAlerts', 'getHostAlerts',
'getUserMacros',
'getHostICAlerts',
'getHostPCAlerts',
'getAcknowledges', 'getAcknowledges',
'getITService', 'getITService',
'acknowledgeEvent', 'acknowledgeEvent',
@@ -91,7 +100,9 @@ export class Zabbix implements ZabbixConnector {
getItemsByIDs: (itemids) => Promise<any>; getItemsByIDs: (itemids) => Promise<any>;
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>; getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>; getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
getHostAlerts: (hostids, applicationids, options?) => Promise<any>; getHostAlerts: (hostids, applicationids, options?) => Promise<ZBXTrigger[]>;
getHostICAlerts: (hostids, applicationids, itemids, options?) => Promise<any>;
getHostPCAlerts: (hostids, applicationids, triggerids, options?) => Promise<any>;
getAcknowledges: (eventids) => Promise<any>; getAcknowledges: (eventids) => Promise<any>;
getITService: (serviceids?) => Promise<any>; getITService: (serviceids?) => Promise<any>;
acknowledgeEvent: (eventid, message) => Promise<any>; acknowledgeEvent: (eventid, message) => Promise<any>;
@@ -99,6 +110,7 @@ export class Zabbix implements ZabbixConnector {
getEventAlerts: (eventids) => Promise<any>; getEventAlerts: (eventids) => Promise<any>;
getExtendedEventData: (eventids) => Promise<any>; getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>; getMacros: (hostids: any[]) => Promise<any>;
getUserMacros: (hostmacroids) => Promise<any>;
getValueMappings: () => Promise<any>; getValueMappings: () => Promise<any>;
getSLAList: () => Promise<any>; getSLAList: () => Promise<any>;
@@ -251,17 +263,38 @@ export class Zabbix implements ZabbixConnector {
return this.getItems(...filters, options); return this.getItems(...filters, options);
} }
getHostsFromTarget(target) { getMacrosFromTarget(target) {
const parts = ['group', 'host', 'application']; const parts = ['group', 'host', 'macro'];
const filters = _.map(parts, (p) => target[p].filter); const filters = _.map(parts, (p) => target[p].filter);
return Promise.all([this.getHosts(...filters), this.getApps(...filters)]).then((results) => { return this.getUMacros(...filters);
}
async getHostsApsFromTarget(target): Promise<[ZBXHost[], ZBXApp[]]> {
const parts = ['group', 'host', 'application'];
const filters = parts.map((p) => target[p].filter);
const results = await Promise.all([this.getHosts(...filters), this.getApps(...filters)]);
const hosts = results[0]; const hosts = results[0];
let apps: AppsResponse = results[1]; let apps: AppsResponse = results[1];
if (apps.appFilterEmpty) { if (apps.appFilterEmpty) {
apps = []; apps = [];
} }
return [hosts, apps]; return [hosts, apps];
}); }
async getHostsFromICTarget(target, options): Promise<[ZBXHost[], ZBXApp[], ZBXItem[]]> {
const parts = ['group', 'host', 'application', 'itemTag', 'item'];
const filters = parts.map((p) => target[p].filter);
const [hosts, apps] = await this.getHostsApsFromTarget(target);
const items = await this.getItems(...filters, options);
return [hosts, apps, items];
}
async getHostsFromPCTarget(target, options): Promise<[ZBXHost[], ZBXApp[], ProblemDTO[]]> {
const parts = ['group', 'host', 'application', 'proxy', 'trigger'];
const filters = parts.map((p) => target[p].filter);
const [hosts, apps] = await this.getHostsApsFromTarget(target);
const problems = await this.getCProblems(...filters, options);
return [hosts, apps, problems];
} }
getAllGroups() { getAllGroups() {
@@ -318,6 +351,17 @@ export class Zabbix implements ZabbixConnector {
}); });
} }
async getAllMacros(groupFilter, hostFilter) {
const hosts = await this.getHosts(groupFilter, hostFilter);
const hostids = hosts?.map((h) => h.hostid);
return this.zabbixAPI.getMacros(hostids);
}
async getUMacros(groupFilter?, hostFilter?, macroFilter?) {
const allMacros = await this.getAllMacros(groupFilter, hostFilter);
return filterByMQuery(allMacros, macroFilter);
}
async getItemTags(groupFilter?, hostFilter?, itemTagFilter?) { async getItemTags(groupFilter?, hostFilter?, itemTagFilter?) {
const items = await this.getAllItems(groupFilter, hostFilter, null, null, {}); const items = await this.getAllItems(groupFilter, hostFilter, null, null, {});
let tags: ZBXItemTag[] = _.flatten( let tags: ZBXItemTag[] = _.flatten(
@@ -480,6 +524,43 @@ export class Zabbix implements ZabbixConnector {
// .then(triggers => this.expandUserMacro.bind(this)(triggers, true)); // .then(triggers => this.expandUserMacro.bind(this)(triggers, true));
} }
getCProblems(groupFilter?, hostFilter?, appFilter?, proxyFilter?, triggerFilter?, options?): Promise<ProblemDTO[]> {
const promises = [
this.getGroups(groupFilter),
this.getHosts(groupFilter, hostFilter),
this.getApps(groupFilter, hostFilter, appFilter),
];
return Promise.all(promises)
.then((results) => {
const [filteredGroups, filteredHosts, filteredApps] = results;
const query: any = {};
if (appFilter) {
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
}
if (hostFilter && hostFilter !== '/.*/') {
query.hostids = _.map(filteredHosts, 'hostid');
}
if (groupFilter) {
query.groupids = _.map(filteredGroups, 'groupid');
}
return query;
})
.then((query) => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options))
.then((problems) => findByFilter(problems, triggerFilter))
.then((problems) => {
const triggerids = problems?.map((problem) => problem.objectid);
return Promise.all([Promise.resolve(problems), this.zabbixAPI.getTriggersByIds(triggerids)]);
})
.then(([problems, triggers]) => joinTriggersWithProblems(problems, triggers))
.then((triggers) => this.filterTriggersByProxy(triggers, proxyFilter));
//.then(triggers => findByFilter(triggers, triggerFilter));
// .then(triggers => this.expandUserMacro.bind(this)(triggers, true));
}
filterTriggersByProxy(triggers, proxyFilter) { filterTriggersByProxy(triggers, proxyFilter) {
return this.getFilteredProxies(proxyFilter).then((proxies) => { return this.getFilteredProxies(proxyFilter).then((proxies) => {
if (proxyFilter && proxyFilter !== '/.*/' && triggers) { if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
@@ -612,6 +693,15 @@ function filterByName(list, name) {
} }
} }
function filterByMacro(list, name) {
const finded = _.filter(list, { macro: name });
if (finded) {
return finded;
} else {
return [];
}
}
function filterByRegex(list, regex) { function filterByRegex(list, regex) {
const filterPattern = utils.buildRegex(regex); const filterPattern = utils.buildRegex(regex);
return _.filter(list, (zbx_obj) => { return _.filter(list, (zbx_obj) => {
@@ -619,6 +709,13 @@ function filterByRegex(list, regex) {
}); });
} }
function filterByMRegex(list, regex) {
const filterPattern = utils.buildRegex(regex);
return _.filter(list, (zbx_obj) => {
return filterPattern.test(zbx_obj?.macro);
});
}
function findByFilter(list, filter) { function findByFilter(list, filter) {
if (utils.isRegex(filter)) { if (utils.isRegex(filter)) {
return filterByRegex(list, filter); return filterByRegex(list, filter);
@@ -635,6 +732,14 @@ function filterByQuery(list, filter) {
} }
} }
function filterByMQuery(list, filter) {
if (utils.isRegex(filter)) {
return filterByMRegex(list, filter);
} else {
return filterByMacro(list, filter);
}
}
function getHostIds(items) { function getHostIds(items) {
const hostIds = _.map(items, (item) => { const hostIds = _.map(items, (item) => {
return _.map(item.hosts, 'hostid'); return _.map(item.hosts, 'hostid');

View File

@@ -6,7 +6,7 @@ import moment from 'moment';
import { isNewProblem, formatLastChange } from '../../utils'; import { isNewProblem, formatLastChange } from '../../utils';
import { ProblemsPanelOptions, TriggerSeverity } from '../../types'; import { ProblemsPanelOptions, TriggerSeverity } from '../../types';
import { AckProblemData, AckModal } from '../AckModal'; import { AckProblemData, AckModal } from '../AckModal';
import EventTag from '../EventTag'; import { EventTag } from '../EventTag';
import AlertAcknowledges from './AlertAcknowledges'; import AlertAcknowledges from './AlertAcknowledges';
import AlertIcon from './AlertIcon'; import AlertIcon from './AlertIcon';
import { ProblemDTO, ZBXTag } from '../../../datasource/types'; import { ProblemDTO, ZBXTag } from '../../../datasource/types';

View File

@@ -1,5 +1,7 @@
import React, { PureComponent } from 'react'; import React from 'react';
import { DataSourceRef } from '@grafana/data'; import { css } from '@emotion/css';
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import { ZBXTag } from '../../datasource/types'; import { ZBXTag } from '../../datasource/types';
const TAG_COLORS = [ const TAG_COLORS = [
@@ -85,39 +87,58 @@ function djb2(str) {
return hash; return hash;
} }
interface EventTagProps { const URLPattern = /^https?:\/\/.+/;
interface Props {
tag: ZBXTag; tag: ZBXTag;
datasource: DataSourceRef | string; datasource: DataSourceRef | string;
highlight?: boolean; highlight?: boolean;
onClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void; onClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void;
} }
export default class EventTag extends PureComponent<EventTagProps> { export const EventTag = ({ tag, datasource, highlight, onClick }: Props) => {
handleClick = (event) => { const styles = useStyles2(getStyles);
if (this.props.onClick) { const onClickInternal = (event) => {
const { tag, datasource } = this.props; if (onClick) {
this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey); onClick(tag, datasource, event.ctrlKey, event.shiftKey);
} }
}; };
render() {
const { tag, highlight } = this.props;
const tagColor = getTagColorsFromName(tag.tag); const tagColor = getTagColorsFromName(tag.tag);
const style: React.CSSProperties = { const style: React.CSSProperties = {
background: tagColor.color, background: tagColor.color,
borderColor: tagColor.borderColor, borderColor: tagColor.borderColor,
}; };
const isUrl = URLPattern.test(tag.value);
let tagElement = <>{tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`}</>;
if (isUrl) {
tagElement = (
<Tooltip placement="top" content={tag.value}>
<a href={tag.value} target="_blank" rel="noreferrer">
<Icon name="link" className={styles.icon} />
{tag.tag}
</a>
</Tooltip>
);
}
return ( return (
// TODO: show tooltip when click feature is fixed // TODO: show tooltip when click feature is fixed
// <Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove"> // <Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove">
<span <span
className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`} className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`}
style={style} style={style}
onClick={this.handleClick} onClick={onClickInternal}
> >
{tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`} {tagElement}
</span> </span>
// </Tooltip> // </Tooltip>
); );
} };
}
const getStyles = (theme: GrafanaTheme2) => ({
icon: css`
margin-right: ${theme.spacing(0.5)};
`,
});

View File

@@ -4,6 +4,7 @@ import { ColorPicker, InlineField, InlineFieldRow, InlineLabel, InlineSwitch, In
import { TriggerSeverity } from '../types'; import { TriggerSeverity } from '../types';
type Props = StandardEditorProps<TriggerSeverity[]>; type Props = StandardEditorProps<TriggerSeverity[]>;
export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => { export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => {
const onSeverityItemChange = (severity: TriggerSeverity) => { const onSeverityItemChange = (severity: TriggerSeverity) => {
value.forEach((v, i) => { value.forEach((v, i) => {

View File

@@ -1,22 +1,26 @@
import React, { FC, PureComponent } from 'react'; import React, { useState, useEffect } from 'react';
import { css } from '@emotion/css';
// eslint-disable-next-line // eslint-disable-next-line
import moment from 'moment'; import moment from 'moment';
import { TimeRange, DataSourceRef } from '@grafana/data'; import { TimeRange, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import { Tooltip } from '@grafana/ui'; import { Tooltip, useStyles2 } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import * as utils from '../../../datasource/utils'; import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource/types';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag, ZBXItem } from '../../../datasource/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types';
import { AckModal, AckProblemData } from '../AckModal'; import { AckModal, AckProblemData } from '../AckModal';
import EventTag from '../EventTag'; import { EventTag } from '../EventTag';
import AcknowledgesList from './AcknowledgesList'; import AcknowledgesList from './AcknowledgesList';
import ProblemTimeline from './ProblemTimeline'; import ProblemTimeline from './ProblemTimeline';
import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components'; import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components';
import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal'; import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal';
import ProblemStatusBar from './ProblemStatusBar'; import ProblemStatusBar from './ProblemStatusBar';
import { RTRow } from '../../types'; import { RTRow } from '../../types';
import { ProblemItems } from './ProblemItems';
import { ProblemHosts, ProblemHostsDescription } from './ProblemHosts';
import { ProblemGroups } from './ProblemGroups';
import { ProblemExpression } from './ProblemExpression';
interface ProblemDetailsProps extends RTRow<ProblemDTO> { interface Props extends RTRow<ProblemDTO> {
rootWidth: number; rootWidth: number;
timeRange: TimeRange; timeRange: TimeRange;
showTimeline?: boolean; showTimeline?: boolean;
@@ -24,95 +28,90 @@ interface ProblemDetailsProps extends RTRow<ProblemDTO> {
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>; getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>; getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>; getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
onExecuteScript(problem: ProblemDTO, scriptid: string): Promise<APIExecuteScriptResponse>; onExecuteScript(problem: ProblemDTO, scriptid: string): Promise<APIExecuteScriptResponse>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
onTagClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void; onTagClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void;
} }
interface ProblemDetailsState { export const ProblemDetails = ({
events: ZBXEvent[]; original,
alerts: ZBXAlert[]; rootWidth,
show: boolean; timeRange,
} showTimeline,
panelId,
getProblemAlerts,
getProblemEvents,
getScripts,
onExecuteScript,
onProblemAck,
onTagClick,
}: Props) => {
const [events, setEvents] = useState([]);
const [alerts, setAletrs] = useState([]);
const [show, setShow] = useState(false);
export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> { useEffect(() => {
constructor(props) { if (showTimeline) {
super(props); fetchProblemEvents();
this.state = {
events: [],
alerts: [],
show: false,
};
} }
fetchProblemAlerts();
componentDidMount() {
if (this.props.showTimeline) {
this.fetchProblemEvents();
}
this.fetchProblemAlerts();
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.setState({ show: true }); setShow(true);
}); });
} }, []);
handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => { const fetchProblemEvents = async () => {
if (this.props.onTagClick) { const problem = original;
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey); const events = await getProblemEvents(problem);
setEvents(events);
};
const fetchProblemAlerts = async () => {
const problem = original;
const alerts = await getProblemAlerts(problem);
setAletrs(alerts);
};
const handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => {
if (onTagClick) {
onTagClick(tag, datasource, ctrlKey, shiftKey);
} }
}; };
fetchProblemEvents() { const ackProblem = (data: AckProblemData) => {
const problem = this.props.original; const problem = original as ProblemDTO;
this.props.getProblemEvents(problem).then((events) => { return onProblemAck(problem, data);
this.setState({ events });
});
}
fetchProblemAlerts() {
const problem = this.props.original;
this.props.getProblemAlerts(problem).then((alerts) => {
this.setState({ alerts });
});
}
ackProblem = (data: AckProblemData) => {
const problem = this.props.original as ProblemDTO;
return this.props.onProblemAck(problem, data);
}; };
getScripts = () => { const getScriptsInternal = () => {
const problem = this.props.original as ProblemDTO; const problem = original as ProblemDTO;
return this.props.getScripts(problem); return getScripts(problem);
}; };
onExecuteScript = (data: ExecScriptData) => { const onExecuteScriptInternal = (data: ExecScriptData) => {
const problem = this.props.original as ProblemDTO; const problem = original as ProblemDTO;
return this.props.onExecuteScript(problem, data.scriptid); return onExecuteScript(problem, data.scriptid);
}; };
render() { const problem = original as ProblemDTO;
const problem = this.props.original as ProblemDTO; const displayClass = show ? 'show' : '';
const alerts = this.state.alerts;
const { rootWidth, panelId, timeRange } = this.props;
const displayClass = this.state.show ? 'show' : '';
const wideLayout = rootWidth > 1200; const wideLayout = rootWidth > 1200;
const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400); const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400);
const age = moment.unix(problem.timestamp).fromNow(true); const age = moment.unix(problem.timestamp).fromNow(true);
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0; const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
const problemSeverity = Number(problem.severity); const problemSeverity = Number(problem.severity);
let dsName: string = this.props.original.datasource as string; let dsName: string = original.datasource as string;
if ((this.props.original.datasource as DataSourceRef)?.uid) { if ((original.datasource as DataSourceRef)?.uid) {
const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid); const dsInstance = getDataSourceSrv().getInstanceSettings((original.datasource as DataSourceRef).uid);
dsName = dsInstance.name; dsName = dsInstance.name;
} }
const styles = useStyles2(getStyles);
return ( return (
<div className={`problem-details-container ${displayClass}`}> <div className={`problem-details-container ${displayClass}`}>
<div className="problem-details-body"> <div className="problem-details-body">
<div className="problem-details"> <div className={styles.problemDetails}>
<div className="problem-details-head"> <div className="problem-details-head">
<div className="problem-actions-left"> <div className="problem-actions-left">
<ExploreButton problem={problem} panelId={panelId} range={timeRange} /> <ExploreButton problem={problem} panelId={panelId} range={timeRange} />
@@ -125,8 +124,8 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
className="problem-action-button" className="problem-action-button"
onClick={() => { onClick={() => {
showModal(ExecScriptModal, { showModal(ExecScriptModal, {
getScripts: this.getScripts, getScripts: getScriptsInternal,
onSubmit: this.onExecuteScript, onSubmit: onExecuteScriptInternal,
onDismiss: hideModal, onDismiss: hideModal,
}); });
}} }}
@@ -141,7 +140,7 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
showModal(AckModal, { showModal(AckModal, {
canClose: problem.manual_close === '1', canClose: problem.manual_close === '1',
severity: problemSeverity, severity: problemSeverity,
onSubmit: this.ackProblem, onSubmit: ackProblem,
onDismiss: hideModal, onDismiss: hideModal,
}); });
}} }}
@@ -164,13 +163,24 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
{problem.comments && ( {problem.comments && (
<div className="problem-description-row"> <div className="problem-description-row">
<div className="problem-description"> <div className="problem-description">
<Tooltip placement="right" content={problem.comments}> <Tooltip placement="right" content={<span dangerouslySetInnerHTML={{ __html: problem.comments }} />}>
<span className="description-label">Description:&nbsp;</span> <span className="description-label">Description:&nbsp;</span>
</Tooltip> </Tooltip>
<span>{problem.comments}</span> {/* <span>{problem.comments}</span> */}
<span dangerouslySetInnerHTML={{ __html: problem.comments }} />
</div> </div>
</div> </div>
)} )}
{problem.items && (
<div className="problem-description-row">
<ProblemExpression problem={problem} />
</div>
)}
{problem.hosts && (
<div className="problem-description-row">
<ProblemHostsDescription hosts={problem.hosts} />
</div>
)}
{problem.tags && problem.tags.length > 0 && ( {problem.tags && problem.tags.length > 0 && (
<div className="problem-tags"> <div className="problem-tags">
{problem.tags && {problem.tags &&
@@ -180,14 +190,12 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
tag={tag} tag={tag}
datasource={problem.datasource} datasource={problem.datasource}
highlight={tag.tag === problem.correlation_tag} highlight={tag.tag === problem.correlation_tag}
onClick={this.handleTagClick} onClick={handleTagClick}
/> />
))} ))}
</div> </div>
)} )}
{this.props.showTimeline && this.state.events.length > 0 && ( {showTimeline && events.length > 0 && <ProblemTimeline events={events} timeRange={timeRange} />}
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange} />
)}
{showAcknowledges && !wideLayout && ( {showAcknowledges && !wideLayout && (
<div className="problem-ack-container"> <div className="problem-ack-container">
<h6> <h6>
@@ -218,86 +226,22 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
<span>{problem.proxy}</span> <span>{problem.proxy}</span>
</div> </div>
)} )}
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />} {problem.groups && <ProblemGroups groups={problem.groups} />}
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />} {problem.hosts && <ProblemHosts hosts={problem.hosts} />}
</div> </div>
</div> </div>
</div> </div>
); );
}
}
interface ProblemItemProps {
item: ZBXItem;
showName?: boolean;
}
function ProblemItem(props: ProblemItemProps) {
const { item, showName } = props;
const itemName = utils.expandItemName(item.name, item.key_);
const tooltipContent = () => (
<>
{itemName}
<br />
{item.lastvalue}
</>
);
return (
<div className="problem-item">
<FAIcon icon="thermometer-three-quarters" />
{showName && <span className="problem-item-name">{item.name}: </span>}
<Tooltip placement="top-start" content={tooltipContent}>
<span className="problem-item-value">{item.lastvalue}</span>
</Tooltip>
</div>
);
}
interface ProblemItemsProps {
items: ZBXItem[];
}
const ProblemItems: FC<ProblemItemsProps> = ({ items }) => {
return (
<div className="problem-items-row">
{items.length > 1 ? (
items.map((item) => <ProblemItem item={item} key={item.itemid} showName={true} />)
) : (
<ProblemItem item={items[0]} />
)}
</div>
);
}; };
interface ProblemGroupsProps { const getStyles = (theme: GrafanaTheme2) => ({
groups: ZBXGroup[]; problemDetails: css`
className?: string; position: relative;
} flex: 10 1 auto;
// padding: 0.5rem 1rem 0.5rem 1.2rem;
class ProblemGroups extends PureComponent<ProblemGroupsProps> { padding: ${theme.spacing(0.5)} ${theme.spacing(1)} ${theme.spacing(0.5)} ${theme.spacing(1.2)}
render() { display: flex;
return this.props.groups.map((g) => ( flex-direction: column;
<div className={this.props.className || ''} key={g.groupid}> // white-space: pre-line;
<FAIcon icon="folder" /> `,
<span>{g.name}</span> });
</div>
));
}
}
interface ProblemHostsProps {
hosts: ZBXHost[];
className?: string;
}
class ProblemHosts extends PureComponent<ProblemHostsProps> {
render() {
return this.props.hosts.map((h) => (
<div className={this.props.className || ''} key={h.hostid}>
<FAIcon icon="server" />
<span>{h.name}</span>
</div>
));
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { css } from '@emotion/css';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { ProblemDTO } from '../../../datasource/types';
interface Props {
problem: ProblemDTO;
}
export const ProblemExpression = ({ problem }: Props) => {
const styles = useStyles2(getStyles);
return (
<>
<Tooltip placement="right" content={problem.expression}>
<span className={styles.label}>Expression:&nbsp;</span>
</Tooltip>
<span className={styles.expression}>{problem.expression}</span>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
label: css`
color: ${theme.colors.text.secondary};
`,
expression: css`
font-family: ${theme.typography.fontFamilyMonospace};
`,
});

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { css } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { FAIcon } from '../../../components';
import { ZBXGroup } from '../../../datasource/types';
interface ProblemGroupsProps {
groups: ZBXGroup[];
className?: string;
}
export const ProblemGroups = ({ groups }: ProblemGroupsProps) => {
const styles = useStyles2(getStyles);
return (
<>
{groups.map((g) => (
<div className={styles.groupContainer} key={g.groupid}>
<FAIcon icon="folder" />
<span>{g.name}</span>
</div>
))}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
groupContainer: css`
margin-bottom: ${theme.spacing(0.2)};
`,
});

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { css } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { FAIcon } from '../../../components';
import { ZBXHost } from '../../../datasource/types';
interface ProblemHostsProps {
hosts: ZBXHost[];
className?: string;
}
export const ProblemHosts = ({ hosts }: ProblemHostsProps) => {
const styles = useStyles2(getStyles);
return (
<>
{hosts.map((h) => (
<div className={styles.hostContainer} key={h.hostid}>
<FAIcon icon="server" />
<span>{h.name}</span>
</div>
))}
</>
);
};
export const ProblemHostsDescription = ({ hosts }: ProblemHostsProps) => {
const styles = useStyles2(getStyles);
return (
<>
<span className={styles.label}>Host Description:&nbsp;</span>
{hosts.map((h, i) => (
<span key={`${h.hostid}-${i}`}>{h.description}</span>
))}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
hostContainer: css`
margin-bottom: ${theme.spacing(0.2)};
`,
label: css`
color: ${theme.colors.text.secondary};
`,
});

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { css } from '@emotion/css';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { FAIcon } from '../../../components';
import { expandItemName } from '../../../datasource/utils';
import { ZBXItem } from '../../../datasource/types';
interface ProblemItemsProps {
items: ZBXItem[];
}
export const ProblemItems = ({ items }: ProblemItemsProps) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.itemsRow}>
{items.length > 1 ? (
items.map((item) => <ProblemItem item={item} key={item.itemid} showName={true} />)
) : (
<ProblemItem item={items[0]} />
)}
</div>
);
};
interface ProblemItemProps {
item: ZBXItem;
showName?: boolean;
}
const ProblemItem = ({ item, showName }: ProblemItemProps) => {
const styles = useStyles2(getStyles);
const itemName = expandItemName(item.name, item.key_);
const tooltipContent = () => (
<>
{itemName}
<br />
{item.lastvalue}
</>
);
return (
<div className={styles.itemContainer}>
<FAIcon icon="thermometer-three-quarters" />
{showName && <span className={styles.itemName}>{item.name}:&nbsp;</span>}
<Tooltip placement="top-start" content={tooltipContent}>
<span>{item.lastvalue}</span>
</Tooltip>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
itemContainer: css`
display: flex;
`,
itemName: css`
color: ${theme.colors.text.secondary};
`,
itemsRow: css`
overflow: hidden;
`,
});

View File

@@ -5,7 +5,7 @@ import _ from 'lodash';
// eslint-disable-next-line // eslint-disable-next-line
import moment from 'moment'; import moment from 'moment';
import { isNewProblem } from '../../utils'; import { isNewProblem } from '../../utils';
import EventTag from '../EventTag'; import { EventTag } from '../EventTag';
import { ProblemDetails } from './ProblemDetails'; import { ProblemDetails } from './ProblemDetails';
import { AckProblemData } from '../AckModal'; import { AckProblemData } from '../AckModal';
import { FAIcon, GFHeartIcon } from '../../../components'; import { FAIcon, GFHeartIcon } from '../../../components';
@@ -173,7 +173,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
Cell: statusIconCell, Cell: statusIconCell,
}, },
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell }, { Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell }, { Header: 'Problem', accessor: 'name', minWidth: 200, Cell: ProblemCell },
{ {
Header: 'Ack', Header: 'Ack',
id: 'ack', id: 'ack',

View File

@@ -102,6 +102,7 @@ export const plugin = new PanelPlugin<ProblemsPanelOptions, {}>(ProblemsPanel)
path: 'triggerSeverity', path: 'triggerSeverity',
name: 'Problem colors', name: 'Problem colors',
editor: ProblemColorEditor, editor: ProblemColorEditor,
defaultValue: defaultPanelOptions.triggerSeverity,
category: ['Colors'], category: ['Colors'],
}) })
.addBooleanSwitch({ .addBooleanSwitch({

View File

@@ -41,7 +41,7 @@ export interface ProblemsPanelOptions {
markAckEvents?: boolean; markAckEvents?: boolean;
} }
export const DEFAULT_SEVERITY = [ export const DEFAULT_SEVERITY: TriggerSeverity[] = [
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true }, { priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true },
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true }, { priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true },
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true }, { priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true },

View File

@@ -51,7 +51,7 @@
"path": "img/screenshot-triggers.png" "path": "img/screenshot-triggers.png"
} }
], ],
"version": "4.2.10", "version": "4.3.0-pre",
"updated": "2022-09-01" "updated": "2022-09-01"
}, },
"includes": [ "includes": [

View File

@@ -237,9 +237,9 @@
transition-property: opacity, max-height; transition-property: opacity, max-height;
&.show { &.show {
max-height: 32rem; max-height: 40rem;
opacity: 1; opacity: 1;
box-shadow: inset -3px 3px 10px $problem-container-shadow; box-shadow: inset -3px 3px 5px #33b5ec4f;
} }
.problem-details-row { .problem-details-row {
@@ -268,6 +268,7 @@
padding: 0.5rem 1rem 0.5rem 1.2rem; padding: 0.5rem 1rem 0.5rem 1.2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
white-space: pre-line;
} }
.problem-description-row { .problem-description-row {
@@ -281,7 +282,7 @@
&:after { &:after {
content: ""; content: "";
text-align: right; text-align: right;
position: absolute; position: inherit;
bottom: 0; bottom: 0;
right: 0; right: 0;
width: 70%; width: 70%;
@@ -292,10 +293,19 @@
.description-label { .description-label {
font-weight: 500; font-weight: 500;
font-style: italic; // font-style: italic;
color: $text-muted; color: $text-muted;
cursor: pointer; cursor: pointer;
} }
.description-delimiter {
border-bottom: solid 2px #f9f9f91c;
margin-top: 10px;
}
.description-expression {
white-space: normal;
}
} }
.problem-age { .problem-age {
@@ -306,11 +316,11 @@
padding-top: 0.8rem; padding-top: 0.8rem;
padding-bottom: 0.8rem; padding-bottom: 0.8rem;
margin-top: auto; margin-top: auto;
position: relative;
} }
.problem-items-row { .problem-items-row {
position: relative; position: inherit;
height: 1.5rem;
overflow: hidden; overflow: hidden;
&:after { &:after {
@@ -326,7 +336,7 @@
} }
.problem-item { .problem-item {
display: flex; display: inherit;
.problem-item-name { .problem-item-name {
color: $text-muted; color: $text-muted;
@@ -334,6 +344,9 @@
.problem-item-value { .problem-item-value {
font-weight: 500; font-weight: 500;
overflow: auto;
display: -webkit-box;
max-height: 60px;
} }
} }
@@ -412,6 +425,7 @@
.problem-ack-list { .problem-ack-list {
display: flex; display: flex;
overflow: auto; overflow: auto;
white-space: pre-line;
.problem-ack-col { .problem-ack-col {
display: flex; display: flex;

View File

@@ -9,7 +9,7 @@ $zbx-card-background-stop: rgba(38, 38, 40, 0.8);
$action-button-color: $blue-dark; $action-button-color: $blue-dark;
$action-button-text-color: $gray-4; $action-button-text-color: $gray-4;
$problems-border-color: #353535; $problems-border-color: #33b5e554;
$problems-table-stripe: $dark-3; $problems-table-stripe: $dark-3;
$problems-table-row-hovered: lighten($problems-table-stripe, 4%); $problems-table-row-hovered: lighten($problems-table-stripe, 4%);
$problems-table-row-hovered-shadow-color: rgba($blue, 0.5); $problems-table-row-hovered-shadow-color: rgba($blue, 0.5);

1030
yarn.lock

File diff suppressed because it is too large Load Diff