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

View File

@@ -82,3 +82,8 @@ sign-package:
yarn sign
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": {
"@emotion/css": "11.1.3",
"@emotion/react": "11.1.5",
"@grafana/data": "9.1.2",
"@grafana/data": "9.3.2",
"@grafana/e2e": "9.2.5",
"@grafana/e2e-selectors": "9.2.5",
"@grafana/eslint-config": "^5.1.0",
"@grafana/runtime": "9.1.2",
"@grafana/runtime": "9.3.2",
"@grafana/toolkit": "9.1.2",
"@grafana/tsconfig": "^1.2.0-rc1",
"@grafana/ui": "9.1.2",
"@grafana/ui": "9.3.2",
"@popperjs/core": "2.4.0",
"@swc/core": "^1.2.144",
"@swc/helpers": "^0.4.12",

View File

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

View File

@@ -201,6 +201,12 @@ export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props)
onChange={onPropChange('acknowledged')}
/>
</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 React, { useEffect } from 'react';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
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 { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { itemTagToString } from '../../utils';
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>> = [
{ value: 0, label: 'Not classified' },
@@ -77,9 +84,80 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
return options;
}, [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
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
useEffect(() => {
fetchGroups();
@@ -93,6 +171,27 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
fetchApps();
}, [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) => {
return (value: string) => {
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 (
<>
<QueryEditorRow>
<InlineField label="Count by" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.countTriggersBy}
options={countByOptions}
onChange={onCountByChange}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
@@ -128,30 +246,87 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
onChange={onFilterChange('host')}
/>
</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>
{(supportsApplications || query.countTriggersBy !== 'items') && (
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</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="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.triggers?.minSeverity}
value={query.options?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
<InlineField label="Count" labelWidth={12}>
<InlineSwitch
value={query.triggers?.count}
onChange={() => onChange({ ...query, triggers: { ...query.triggers, count: !query.triggers?.count } })}
value={query.options?.count}
onChange={() => onChange({ ...query, options: { ...query.options, count: !query.options?.count } })}
/>
</InlineField>
</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_TRIGGERS = '4';
export const MODE_PROBLEMS = '5';
export const MODE_MACROS = '6';
// Triggers severity
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);
} else if (target.queryType === c.MODE_TRIGGERS) {
// Triggers query
return this.queryTriggersData(target, timeRange);
return this.queryTriggersData(target, timeRange, request);
} else if (target.queryType === c.MODE_PROBLEMS) {
// Problems query
return this.queryProblems(target, timeRange, request);
} else if (target.queryType === c.MODE_MACROS) {
// UserMacro query
return this.queryUserMacrosData(target);
} else {
return [];
}
@@ -254,7 +257,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return Promise.all(_.flatten(promises))
.then(_.flatten)
.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);
if (responseHandler.isConvertibleToWide(data)) {
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);
}
queryTriggersData(target, timeRange) {
async queryUserMacrosData(target) {
const groupFilter = target.group.filter;
const hostFilter = target.host.filter;
const macroFilter = target.macro.filter;
const macros = await this.zabbix.getUMacros(groupFilter, hostFilter, macroFilter);
const hostmacroids = _.map(macros, 'hostmacroid');
const userMacros = await this.zabbix.getUserMacros(hostmacroids);
return responseHandler.handleMacro(userMacros, target);
}
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([]);
}
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;
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;
return Promise.all([
this.zabbix.getHostAlerts(hostids, appids, options),
this.zabbix.getGroups(groupFilter),
]).then(([triggers, groups]) => {
return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
});
} else {
return Promise.resolve([]);
}
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) {
@@ -742,6 +841,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
templateSrv.variableExists(target.application?.filter) ||
templateSrv.variableExists(target.itemTag?.filter) ||
templateSrv.variableExists(target.item?.filter) ||
templateSrv.variableExists(target.macro?.filter) ||
templateSrv.variableExists(target.proxy?.filter) ||
templateSrv.variableExists(target.trigger?.filter) ||
templateSrv.variableExists(target.textFilter) ||
@@ -972,3 +1072,17 @@ function getRequestTarget(request: DataQueryRequest<any>, refId: string): any {
}
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 * as c from './constants';
export const DS_QUERY_SCHEMA = 11;
export const DS_CONFIG_SCHEMA = 3;
/**
* Query format 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.application.filter = target.application.name === '*' ? '' : target.application.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;
}
@@ -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) {
target.resultFormat = target.resultFormat || 'time_series';
target = fixTargetGroup(target);
@@ -97,6 +127,10 @@ export function migrate(target) {
migrateProblemSort(target);
migrateApplications(target);
migrateSLAProperty(target);
migrateTriggersMode(target);
migrateNewTriggersCountModes(target);
target.schema = DS_QUERY_SCHEMA;
return target;
}
@@ -115,8 +149,6 @@ function convertToRegex(str) {
}
}
export const DS_CONFIG_SCHEMA = 3;
export function migrateDSConfig(jsonData) {
if (!jsonData) {
jsonData = {};

View File

@@ -16,7 +16,7 @@ import {
TIME_SERIES_TIME_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME,
} from '@grafana/data';
import { ZabbixMetricsQuery } from './types';
import { ZabbixMetricsQuery, ZBXGroup, ZBXTrigger } from './types';
/**
* 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(
timeseries,
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)) {
let triggersCount = null;
try {
@@ -597,29 +620,47 @@ function handleTriggersResponse(triggers, groups, timeRange) {
} catch (err) {
console.log('Error when handling triggers count: ', err);
}
return {
target: 'triggers count',
datapoints: [[triggersCount, timeRange[1] * 1000]],
};
const frame = new MutableDataFrame({
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 {
const stats = getTriggerStats(triggers);
const groupNames = _.map(groups, 'name');
const table: any = new TableModel();
table.addColumn({ text: 'Host group' });
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
table.addColumn({ text: severity.text });
const frame = new MutableDataFrame({
refId: target.refId,
fields: [{ name: 'Host group', type: FieldType.string, 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);
}
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(),
});
}
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,
handleTrends,
handleText,
handleMacro,
handleHistoryAsTable,
handleSLAResponse,
handleTriggersResponse,

View File

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

View File

@@ -3,7 +3,15 @@ import _ from 'lodash';
import moment from 'moment';
import * as c from './constants';
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
@@ -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.
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) {

View File

@@ -3,7 +3,7 @@ import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils';
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 { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
import { rangeUtil } from '@grafana/data';
@@ -228,6 +228,15 @@ export class ZabbixAPIConnector {
return this.request('usermacro.get', params);
}
getUserMacros(hostmacroids) {
const params = {
output: 'extend',
hostmacroids: hostmacroids,
selectHosts: ['hostid', 'name'],
};
return this.request('usermacro.get', params);
}
getGlobalMacros() {
const params = {
output: 'extend',
@@ -506,10 +515,11 @@ export class ZabbixAPIConnector {
expandDescription: true,
expandData: true,
expandComment: true,
expandExpression: true,
monitored: true,
skipDependent: true,
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'],
// selectLastEvent: 'extend',
// selectTags: 'extend',
@@ -680,7 +690,7 @@ export class ZabbixAPIConnector {
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 params: any = {
output: 'extend',
@@ -697,6 +707,100 @@ export class ZabbixAPIConnector {
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) {
params.countOutput = true;
}

View File

@@ -5,18 +5,22 @@ export interface ZabbixConnector {
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
getAlerts: (itemids, timeFrom?, timeTo?) => 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>;
getITService: (serviceids?) => Promise<any>;
acknowledgeEvent: (eventid, message) => Promise<any>;
getProxies: () => Promise<any>;
getEventAlerts: (eventids) => Promise<any>;
getExtendedEventData: (eventids) => Promise<any>;
getUserMacros: (hostmacroids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>;
getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any;
getApps: (groupFilter?, hostFilter?, appFilter?) => any;
getUMacros: (groupFilter?, hostFilter?, macroFilter?) => any;
getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, 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 { ZabbixConnector } from './types';
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> {
appFilterEmpty?: boolean;
@@ -26,13 +26,18 @@ const REQUESTS_TO_PROXYFY = [
'getApps',
'getItems',
'getMacros',
'getUMacros',
'getItemsByIDs',
'getEvents',
'getAlerts',
'getHostAlerts',
'getUserMacros',
'getHostICAlerts',
'getHostPCAlerts',
'getAcknowledges',
'getITService',
'getSLA',
'getVersion',
'getProxies',
'getEventAlerts',
'getExtendedEventData',
@@ -50,6 +55,7 @@ const REQUESTS_TO_CACHE = [
'getApps',
'getItems',
'getMacros',
'getUMacros',
'getItemsByIDs',
'getITService',
'getProxies',
@@ -65,6 +71,9 @@ const REQUESTS_TO_BIND = [
'getEvents',
'getAlerts',
'getHostAlerts',
'getUserMacros',
'getHostICAlerts',
'getHostPCAlerts',
'getAcknowledges',
'getITService',
'acknowledgeEvent',
@@ -91,7 +100,9 @@ export class Zabbix implements ZabbixConnector {
getItemsByIDs: (itemids) => Promise<any>;
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => 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>;
getITService: (serviceids?) => Promise<any>;
acknowledgeEvent: (eventid, message) => Promise<any>;
@@ -99,6 +110,7 @@ export class Zabbix implements ZabbixConnector {
getEventAlerts: (eventids) => Promise<any>;
getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>;
getUserMacros: (hostmacroids) => Promise<any>;
getValueMappings: () => Promise<any>;
getSLAList: () => Promise<any>;
@@ -251,17 +263,38 @@ export class Zabbix implements ZabbixConnector {
return this.getItems(...filters, options);
}
getHostsFromTarget(target) {
const parts = ['group', 'host', 'application'];
getMacrosFromTarget(target) {
const parts = ['group', 'host', 'macro'];
const filters = _.map(parts, (p) => target[p].filter);
return Promise.all([this.getHosts(...filters), this.getApps(...filters)]).then((results) => {
const hosts = results[0];
let apps: AppsResponse = results[1];
if (apps.appFilterEmpty) {
apps = [];
}
return [hosts, apps];
});
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];
let apps: AppsResponse = results[1];
if (apps.appFilterEmpty) {
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() {
@@ -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?) {
const items = await this.getAllItems(groupFilter, hostFilter, null, null, {});
let tags: ZBXItemTag[] = _.flatten(
@@ -480,6 +524,43 @@ export class Zabbix implements ZabbixConnector {
// .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) {
return this.getFilteredProxies(proxyFilter).then((proxies) => {
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) {
const filterPattern = utils.buildRegex(regex);
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) {
if (utils.isRegex(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) {
const hostIds = _.map(items, (item) => {
return _.map(item.hosts, 'hostid');

View File

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

View File

@@ -1,5 +1,7 @@
import React, { PureComponent } from 'react';
import { DataSourceRef } from '@grafana/data';
import React from 'react';
import { css } from '@emotion/css';
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import { ZBXTag } from '../../datasource/types';
const TAG_COLORS = [
@@ -85,39 +87,58 @@ function djb2(str) {
return hash;
}
interface EventTagProps {
const URLPattern = /^https?:\/\/.+/;
interface Props {
tag: ZBXTag;
datasource: DataSourceRef | string;
highlight?: boolean;
onClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
export default class EventTag extends PureComponent<EventTagProps> {
handleClick = (event) => {
if (this.props.onClick) {
const { tag, datasource } = this.props;
this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey);
export const EventTag = ({ tag, datasource, highlight, onClick }: Props) => {
const styles = useStyles2(getStyles);
const onClickInternal = (event) => {
if (onClick) {
onClick(tag, datasource, event.ctrlKey, event.shiftKey);
}
};
render() {
const { tag, highlight } = this.props;
const tagColor = getTagColorsFromName(tag.tag);
const style: React.CSSProperties = {
background: tagColor.color,
borderColor: tagColor.borderColor,
};
return (
// TODO: show tooltip when click feature is fixed
// <Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove">
<span
className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`}
style={style}
onClick={this.handleClick}
>
{tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`}
</span>
// </Tooltip>
const tagColor = getTagColorsFromName(tag.tag);
const style: React.CSSProperties = {
background: tagColor.color,
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 (
// TODO: show tooltip when click feature is fixed
// <Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove">
<span
className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`}
style={style}
onClick={onClickInternal}
>
{tagElement}
</span>
// </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';
type Props = StandardEditorProps<TriggerSeverity[]>;
export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => {
const onSeverityItemChange = (severity: TriggerSeverity) => {
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
import moment from 'moment';
import { TimeRange, DataSourceRef } from '@grafana/data';
import { Tooltip } from '@grafana/ui';
import { TimeRange, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
import * as utils from '../../../datasource/utils';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag, ZBXItem } from '../../../datasource/types';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types';
import { AckModal, AckProblemData } from '../AckModal';
import EventTag from '../EventTag';
import { EventTag } from '../EventTag';
import AcknowledgesList from './AcknowledgesList';
import ProblemTimeline from './ProblemTimeline';
import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components';
import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal';
import ProblemStatusBar from './ProblemStatusBar';
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;
timeRange: TimeRange;
showTimeline?: boolean;
@@ -24,280 +28,220 @@ interface ProblemDetailsProps extends RTRow<ProblemDTO> {
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
onExecuteScript(problem: ProblemDTO, scriptid: string): Promise<APIExecuteScriptResponse>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
onTagClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
interface ProblemDetailsState {
events: ZBXEvent[];
alerts: ZBXAlert[];
show: boolean;
}
export const ProblemDetails = ({
original,
rootWidth,
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> {
constructor(props) {
super(props);
this.state = {
events: [],
alerts: [],
show: false,
};
}
componentDidMount() {
if (this.props.showTimeline) {
this.fetchProblemEvents();
useEffect(() => {
if (showTimeline) {
fetchProblemEvents();
}
this.fetchProblemAlerts();
fetchProblemAlerts();
requestAnimationFrame(() => {
this.setState({ show: true });
setShow(true);
});
}
}, []);
handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
const fetchProblemEvents = async () => {
const problem = original;
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 problem = this.props.original;
this.props.getProblemEvents(problem).then((events) => {
this.setState({ events });
});
const ackProblem = (data: AckProblemData) => {
const problem = original as ProblemDTO;
return onProblemAck(problem, data);
};
const getScriptsInternal = () => {
const problem = original as ProblemDTO;
return getScripts(problem);
};
const onExecuteScriptInternal = (data: ExecScriptData) => {
const problem = original as ProblemDTO;
return onExecuteScript(problem, data.scriptid);
};
const problem = original as ProblemDTO;
const displayClass = show ? 'show' : '';
const wideLayout = rootWidth > 1200;
const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400);
const age = moment.unix(problem.timestamp).fromNow(true);
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
const problemSeverity = Number(problem.severity);
let dsName: string = original.datasource as string;
if ((original.datasource as DataSourceRef)?.uid) {
const dsInstance = getDataSourceSrv().getInstanceSettings((original.datasource as DataSourceRef).uid);
dsName = dsInstance.name;
}
const styles = useStyles2(getStyles);
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 problem = this.props.original as ProblemDTO;
return this.props.getScripts(problem);
};
onExecuteScript = (data: ExecScriptData) => {
const problem = this.props.original as ProblemDTO;
return this.props.onExecuteScript(problem, data.scriptid);
};
render() {
const problem = this.props.original as ProblemDTO;
const alerts = this.state.alerts;
const { rootWidth, panelId, timeRange } = this.props;
const displayClass = this.state.show ? 'show' : '';
const wideLayout = rootWidth > 1200;
const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400);
const age = moment.unix(problem.timestamp).fromNow(true);
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
const problemSeverity = Number(problem.severity);
let dsName: string = this.props.original.datasource as string;
if ((this.props.original.datasource as DataSourceRef)?.uid) {
const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid);
dsName = dsInstance.name;
}
return (
<div className={`problem-details-container ${displayClass}`}>
<div className="problem-details-body">
<div className="problem-details">
<div className="problem-details-head">
<div className="problem-actions-left">
<ExploreButton problem={problem} panelId={panelId} range={timeRange} />
</div>
{problem.showAckButton && (
<div className="problem-actions">
<ModalController>
{({ showModal, hideModal }) => (
<ExecScriptButton
className="problem-action-button"
onClick={() => {
showModal(ExecScriptModal, {
getScripts: this.getScripts,
onSubmit: this.onExecuteScript,
onDismiss: hideModal,
});
}}
/>
)}
</ModalController>
<ModalController>
{({ showModal, hideModal }) => (
<AckButton
className="problem-action-button"
onClick={() => {
showModal(AckModal, {
canClose: problem.manual_close === '1',
severity: problemSeverity,
onSubmit: this.ackProblem,
onDismiss: hideModal,
});
}}
/>
)}
</ModalController>
</div>
)}
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
return (
<div className={`problem-details-container ${displayClass}`}>
<div className="problem-details-body">
<div className={styles.problemDetails}>
<div className="problem-details-head">
<div className="problem-actions-left">
<ExploreButton problem={problem} panelId={panelId} range={timeRange} />
</div>
<div className="problem-details-row">
<div className="problem-value-container">
<div className="problem-age">
<FAIcon icon="clock-o" />
<span>{age}</span>
</div>
{problem.items && <ProblemItems items={problem.items} />}
</div>
</div>
{problem.comments && (
<div className="problem-description-row">
<div className="problem-description">
<Tooltip placement="right" content={problem.comments}>
<span className="description-label">Description:&nbsp;</span>
</Tooltip>
<span>{problem.comments}</span>
</div>
</div>
)}
{problem.tags && problem.tags.length > 0 && (
<div className="problem-tags">
{problem.tags &&
problem.tags.map((tag) => (
<EventTag
key={tag.tag + tag.value}
tag={tag}
datasource={problem.datasource}
highlight={tag.tag === problem.correlation_tag}
onClick={this.handleTagClick}
{problem.showAckButton && (
<div className="problem-actions">
<ModalController>
{({ showModal, hideModal }) => (
<ExecScriptButton
className="problem-action-button"
onClick={() => {
showModal(ExecScriptModal, {
getScripts: getScriptsInternal,
onSubmit: onExecuteScriptInternal,
onDismiss: hideModal,
});
}}
/>
))}
</div>
)}
{this.props.showTimeline && this.state.events.length > 0 && (
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange} />
)}
{showAcknowledges && !wideLayout && (
<div className="problem-ack-container">
<h6>
<FAIcon icon="reply-all" /> Acknowledges
</h6>
<AcknowledgesList acknowledges={problem.acknowledges} />
)}
</ModalController>
<ModalController>
{({ showModal, hideModal }) => (
<AckButton
className="problem-action-button"
onClick={() => {
showModal(AckModal, {
canClose: problem.manual_close === '1',
severity: problemSeverity,
onSubmit: ackProblem,
onDismiss: hideModal,
});
}}
/>
)}
</ModalController>
</div>
)}
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
</div>
{showAcknowledges && wideLayout && (
<div className="problem-details-middle">
<div className="problem-ack-container">
<h6>
<FAIcon icon="reply-all" /> Acknowledges
</h6>
<AcknowledgesList acknowledges={problem.acknowledges} />
<div className="problem-details-row">
<div className="problem-value-container">
<div className="problem-age">
<FAIcon icon="clock-o" />
<span>{age}</span>
</div>
{problem.items && <ProblemItems items={problem.items} />}
</div>
</div>
{problem.comments && (
<div className="problem-description-row">
<div className="problem-description">
<Tooltip placement="right" content={<span dangerouslySetInnerHTML={{ __html: problem.comments }} />}>
<span className="description-label">Description:&nbsp;</span>
</Tooltip>
{/* <span>{problem.comments}</span> */}
<span dangerouslySetInnerHTML={{ __html: problem.comments }} />
</div>
</div>
)}
<div className="problem-details-right">
<div className="problem-details-right-item">
<FAIcon icon="database" />
<span>{dsName}</span>
{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 && (
<div className="problem-tags">
{problem.tags &&
problem.tags.map((tag) => (
<EventTag
key={tag.tag + tag.value}
tag={tag}
datasource={problem.datasource}
highlight={tag.tag === problem.correlation_tag}
onClick={handleTagClick}
/>
))}
</div>
)}
{showTimeline && events.length > 0 && <ProblemTimeline events={events} timeRange={timeRange} />}
{showAcknowledges && !wideLayout && (
<div className="problem-ack-container">
<h6>
<FAIcon icon="reply-all" /> Acknowledges
</h6>
<AcknowledgesList acknowledges={problem.acknowledges} />
</div>
)}
</div>
{showAcknowledges && wideLayout && (
<div className="problem-details-middle">
<div className="problem-ack-container">
<h6>
<FAIcon icon="reply-all" /> Acknowledges
</h6>
<AcknowledgesList acknowledges={problem.acknowledges} />
</div>
{problem.proxy && (
<div className="problem-details-right-item">
<FAIcon icon="cloud" />
<span>{problem.proxy}</span>
</div>
)}
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
</div>
)}
<div className="problem-details-right">
<div className="problem-details-right-item">
<FAIcon icon="database" />
<span>{dsName}</span>
</div>
{problem.proxy && (
<div className="problem-details-right-item">
<FAIcon icon="cloud" />
<span>{problem.proxy}</span>
</div>
)}
{problem.groups && <ProblemGroups groups={problem.groups} />}
{problem.hosts && <ProblemHosts hosts={problem.hosts} />}
</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 {
groups: ZBXGroup[];
className?: string;
}
class ProblemGroups extends PureComponent<ProblemGroupsProps> {
render() {
return this.props.groups.map((g) => (
<div className={this.props.className || ''} key={g.groupid}>
<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>
));
}
}
const getStyles = (theme: GrafanaTheme2) => ({
problemDetails: css`
position: relative;
flex: 10 1 auto;
// padding: 0.5rem 1rem 0.5rem 1.2rem;
padding: ${theme.spacing(0.5)} ${theme.spacing(1)} ${theme.spacing(0.5)} ${theme.spacing(1.2)}
display: flex;
flex-direction: column;
// white-space: pre-line;
`,
});

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
import moment from 'moment';
import { isNewProblem } from '../../utils';
import EventTag from '../EventTag';
import { EventTag } from '../EventTag';
import { ProblemDetails } from './ProblemDetails';
import { AckProblemData } from '../AckModal';
import { FAIcon, GFHeartIcon } from '../../../components';
@@ -173,7 +173,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
Cell: statusIconCell,
},
{ 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',
id: 'ack',

View File

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

View File

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

View File

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

View File

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

1030
yarn.lock

File diff suppressed because it is too large Load Diff