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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
131
src/datasource/components/QueryEditor/UserMacrosQueryEditor.tsx
Normal file
131
src/datasource/components/QueryEditor/UserMacrosQueryEditor.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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: </span>
|
<span className="description-label">Description: </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>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
30
src/panel-triggers/components/Problems/ProblemExpression.tsx
Normal file
30
src/panel-triggers/components/Problems/ProblemExpression.tsx
Normal 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: </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};
|
||||||
|
`,
|
||||||
|
});
|
||||||
31
src/panel-triggers/components/Problems/ProblemGroups.tsx
Normal file
31
src/panel-triggers/components/Problems/ProblemGroups.tsx
Normal 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)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
46
src/panel-triggers/components/Problems/ProblemHosts.tsx
Normal file
46
src/panel-triggers/components/Problems/ProblemHosts.tsx
Normal 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: </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};
|
||||||
|
`,
|
||||||
|
});
|
||||||
63
src/panel-triggers/components/Problems/ProblemItems.tsx
Normal file
63
src/panel-triggers/components/Problems/ProblemItems.tsx
Normal 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}: </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;
|
||||||
|
`,
|
||||||
|
});
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user