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
|
||||
|
||||
*.bat
|
||||
.DS_Store
|
||||
|
||||
# Grafana linter config
|
||||
# .jshintrc
|
||||
@@ -38,6 +39,7 @@ yarn-error.log
|
||||
# Built plugin
|
||||
dist/
|
||||
ci/
|
||||
alexanderzobnin-zabbix-app.zip
|
||||
|
||||
# Grafana toolkit configs
|
||||
# .prettierrc.js
|
||||
|
||||
5
Makefile
5
Makefile
@@ -82,3 +82,8 @@ sign-package:
|
||||
yarn sign
|
||||
|
||||
package: install dist sign-package
|
||||
|
||||
zip:
|
||||
cp -r dist/ alexanderzobnin-zabbix-app
|
||||
zip -r alexanderzobnin-zabbix-app.zip alexanderzobnin-zabbix-app
|
||||
rm -rf alexanderzobnin-zabbix-app
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"devDependencies": {
|
||||
"@emotion/css": "11.1.3",
|
||||
"@emotion/react": "11.1.5",
|
||||
"@grafana/data": "9.1.2",
|
||||
"@grafana/data": "9.3.2",
|
||||
"@grafana/e2e": "9.2.5",
|
||||
"@grafana/e2e-selectors": "9.2.5",
|
||||
"@grafana/eslint-config": "^5.1.0",
|
||||
"@grafana/runtime": "9.1.2",
|
||||
"@grafana/runtime": "9.3.2",
|
||||
"@grafana/toolkit": "9.1.2",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@grafana/ui": "9.1.2",
|
||||
"@grafana/ui": "9.3.2",
|
||||
"@popperjs/core": "2.4.0",
|
||||
"@swc/core": "^1.2.144",
|
||||
"@swc/helpers": "^0.4.12",
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
|
||||
import * as c from '../constants';
|
||||
import * as migrations from '../migrations';
|
||||
import { migrate, DS_QUERY_SCHEMA } from '../migrations';
|
||||
import { ZabbixDatasource } from '../datasource';
|
||||
import { ShowProblemTypes, ZabbixDSOptions, ZabbixMetricsQuery, ZabbixQueryOptions } from '../types';
|
||||
import { MetricsQueryEditor } from './QueryEditor/MetricsQueryEditor';
|
||||
@@ -13,6 +13,7 @@ import { ProblemsQueryEditor } from './QueryEditor/ProblemsQueryEditor';
|
||||
import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor';
|
||||
import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
|
||||
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
|
||||
import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor';
|
||||
|
||||
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
|
||||
{
|
||||
@@ -38,29 +39,32 @@ const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
|
||||
{
|
||||
value: c.MODE_TRIGGERS,
|
||||
label: 'Triggers',
|
||||
description: 'Query triggers data',
|
||||
description: 'Count triggers',
|
||||
},
|
||||
{
|
||||
value: c.MODE_PROBLEMS,
|
||||
label: 'Problems',
|
||||
description: 'Query problems',
|
||||
},
|
||||
{
|
||||
value: c.MODE_MACROS,
|
||||
label: 'User macros',
|
||||
description: 'User Macros',
|
||||
},
|
||||
];
|
||||
|
||||
const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
|
||||
schema: DS_QUERY_SCHEMA,
|
||||
queryType: c.MODE_METRICS,
|
||||
group: { filter: '' },
|
||||
host: { filter: '' },
|
||||
application: { filter: '' },
|
||||
itemTag: { filter: '' },
|
||||
item: { filter: '' },
|
||||
macro: { filter: '' },
|
||||
functions: [],
|
||||
triggers: {
|
||||
count: true,
|
||||
minSeverity: 3,
|
||||
acknowledged: 2,
|
||||
},
|
||||
trigger: { filter: '' },
|
||||
countTriggersBy: '',
|
||||
tags: { filter: '' },
|
||||
proxy: { filter: '' },
|
||||
textFilter: '',
|
||||
@@ -70,6 +74,7 @@ const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
|
||||
disableDataAlignment: false,
|
||||
useZabbixValueMapping: false,
|
||||
useTrends: 'default',
|
||||
count: false,
|
||||
},
|
||||
table: {
|
||||
skipEmptyValues: false,
|
||||
@@ -96,6 +101,7 @@ function getProblemsQueryDefaults(): Partial<ZabbixMetricsQuery> {
|
||||
hostProxy: false,
|
||||
limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT,
|
||||
useTimeRange: false,
|
||||
count: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -119,7 +125,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
||||
|
||||
// Migrate query on load
|
||||
useEffect(() => {
|
||||
const migratedQuery = migrations.migrate(query);
|
||||
const migratedQuery = migrate(query);
|
||||
onChange(migratedQuery);
|
||||
}, []);
|
||||
|
||||
@@ -184,6 +190,10 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
||||
return <TriggersQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
|
||||
};
|
||||
|
||||
const renderUserMacrosEditor = () => {
|
||||
return <UserMacrosQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
@@ -206,6 +216,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
||||
{queryType === c.MODE_ITSERVICE && renderITServicesEditor()}
|
||||
{queryType === c.MODE_PROBLEMS && renderProblemsEditor()}
|
||||
{queryType === c.MODE_TRIGGERS && renderTriggersEditor()}
|
||||
{queryType === c.MODE_MACROS && renderUserMacrosEditor()}
|
||||
<QueryOptionsEditor queryType={queryType} queryOptions={query.options} onChange={onOptionsChange} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -201,6 +201,12 @@ export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props)
|
||||
onChange={onPropChange('acknowledged')}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Use time range" labelWidth={24}>
|
||||
<InlineSwitch
|
||||
value={queryOptions.useTimeRange}
|
||||
onChange={() => onChange({ ...queryOptions, useTimeRange: !queryOptions.useTimeRange })}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, FormEvent } from 'react';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineField, InlineSwitch, Select } from '@grafana/ui';
|
||||
import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
|
||||
import { QueryEditorRow } from './QueryEditorRow';
|
||||
import { MetricPicker } from '../../../components';
|
||||
import { getVariableOptions } from './utils';
|
||||
import { itemTagToString } from '../../utils';
|
||||
import { ZabbixDatasource } from '../../datasource';
|
||||
import { ZabbixMetricsQuery } from '../../types';
|
||||
import { ZabbixMetricsQuery, ZBXItem, ZBXItemTag } from '../../types';
|
||||
|
||||
const countByOptions: Array<SelectableValue<string>> = [
|
||||
{ value: '', label: 'All triggers' },
|
||||
{ value: 'problems', label: 'Problems' },
|
||||
{ value: 'items', label: 'Items' },
|
||||
];
|
||||
|
||||
const severityOptions: Array<SelectableValue<number>> = [
|
||||
{ value: 0, label: 'Not classified' },
|
||||
@@ -77,9 +84,80 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
return options;
|
||||
}, [query.group.filter, query.host.filter]);
|
||||
|
||||
const loadTagOptions = async (group: string, host: string) => {
|
||||
if (!datasource.zabbix.isZabbix54OrHigher()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupFilter = datasource.replaceTemplateVars(group);
|
||||
const hostFilter = datasource.replaceTemplateVars(host);
|
||||
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, null, null, {});
|
||||
const tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => item.tags || []));
|
||||
|
||||
const tagList = _.uniqBy(tags, (t) => t.tag + t.value || '').map((t) => itemTagToString(t));
|
||||
let options: Array<SelectableValue<string>> = tagList?.map((tag) => ({
|
||||
value: tag,
|
||||
label: tag,
|
||||
}));
|
||||
options = _.uniqBy(options, (o) => o.value);
|
||||
options.unshift(...getVariableOptions());
|
||||
return options;
|
||||
};
|
||||
|
||||
const [{ loading: tagsLoading, value: tagOptions }, fetchItemTags] = useAsyncFn(async () => {
|
||||
const options = await loadTagOptions(query.group.filter, query.host.filter);
|
||||
return options;
|
||||
}, [query.group.filter, query.host.filter]);
|
||||
|
||||
const loadProxyOptions = async () => {
|
||||
const proxies = await datasource.zabbix.getProxies();
|
||||
const options = proxies?.map((proxy) => ({
|
||||
value: proxy.host,
|
||||
label: proxy.host,
|
||||
}));
|
||||
options.unshift(...getVariableOptions());
|
||||
return options;
|
||||
};
|
||||
|
||||
const [{ loading: proxiesLoading, value: proxiesOptions }, fetchProxies] = useAsyncFn(async () => {
|
||||
const options = await loadProxyOptions();
|
||||
return options;
|
||||
}, []);
|
||||
|
||||
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => {
|
||||
const groupFilter = datasource.replaceTemplateVars(group);
|
||||
const hostFilter = datasource.replaceTemplateVars(host);
|
||||
const appFilter = datasource.replaceTemplateVars(app);
|
||||
const tagFilter = datasource.replaceTemplateVars(itemTag);
|
||||
const options = {
|
||||
itemtype: 'num',
|
||||
showDisabledItems: query.options.showDisabledItems,
|
||||
};
|
||||
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
|
||||
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}));
|
||||
itemOptions = _.uniqBy(itemOptions, (o) => o.value);
|
||||
itemOptions.unshift(...getVariableOptions());
|
||||
return itemOptions;
|
||||
};
|
||||
|
||||
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
|
||||
const options = await loadItemOptions(
|
||||
query.group.filter,
|
||||
query.host.filter,
|
||||
query.application.filter,
|
||||
query.itemTag.filter
|
||||
);
|
||||
return options;
|
||||
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]);
|
||||
|
||||
// Update suggestions on every metric change
|
||||
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
|
||||
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
|
||||
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
|
||||
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
@@ -93,6 +171,27 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
fetchApps();
|
||||
}, [groupFilter, hostFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItemTags();
|
||||
}, [groupFilter, hostFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProxies();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, [groupFilter, hostFilter, appFilter, tagFilter]);
|
||||
|
||||
const onTextFilterChange = (prop: string) => {
|
||||
return (v: FormEvent<HTMLInputElement>) => {
|
||||
const newValue = v?.currentTarget?.value;
|
||||
if (newValue !== null) {
|
||||
onChange({ ...query, [prop]: { filter: newValue } });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onFilterChange = (prop: string) => {
|
||||
return (value: string) => {
|
||||
if (value !== null) {
|
||||
@@ -107,8 +206,27 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onCountByChange = (option: SelectableValue) => {
|
||||
if (option.value !== null) {
|
||||
onChange({ ...query, countTriggersBy: option.value! });
|
||||
}
|
||||
};
|
||||
|
||||
const supportsApplications = datasource.zabbix.supportsApplications();
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryEditorRow>
|
||||
<InlineField label="Count by" labelWidth={12}>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
width={24}
|
||||
value={query.countTriggersBy}
|
||||
options={countByOptions}
|
||||
onChange={onCountByChange}
|
||||
/>
|
||||
</InlineField>
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow>
|
||||
<InlineField label="Group" labelWidth={12}>
|
||||
<MetricPicker
|
||||
@@ -128,8 +246,20 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
onChange={onFilterChange('host')}
|
||||
/>
|
||||
</InlineField>
|
||||
{query.countTriggersBy === 'problems' && (
|
||||
<InlineField label="Proxy" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.proxy?.filter}
|
||||
options={proxiesOptions}
|
||||
isLoading={proxiesLoading}
|
||||
onChange={onFilterChange('proxy')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow>
|
||||
{(supportsApplications || query.countTriggersBy !== 'items') && (
|
||||
<InlineField label="Application" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
@@ -139,19 +269,64 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
onChange={onFilterChange('application')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
{!supportsApplications && query.countTriggersBy === 'items' && (
|
||||
<InlineField label="Item tag" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.itemTag.filter}
|
||||
options={tagOptions}
|
||||
isLoading={tagsLoading}
|
||||
onChange={onFilterChange('itemTag')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
{query.countTriggersBy === 'problems' && (
|
||||
<>
|
||||
<InlineField label="Problem" labelWidth={12}>
|
||||
<Input
|
||||
width={24}
|
||||
defaultValue={query.trigger?.filter}
|
||||
placeholder="Problem name"
|
||||
onBlur={onTextFilterChange('trigger')}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Tags" labelWidth={12}>
|
||||
<Input
|
||||
width={24}
|
||||
defaultValue={query.tags?.filter}
|
||||
placeholder="tag1:value1, tag2:value2"
|
||||
onBlur={onTextFilterChange('tags')}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
{query.countTriggersBy === 'items' && (
|
||||
<InlineField label="Item" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.item.filter}
|
||||
options={itemOptions}
|
||||
isLoading={itemsLoading}
|
||||
onChange={onFilterChange('item')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow>
|
||||
<InlineField label="Min severity" labelWidth={12}>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
width={24}
|
||||
value={query.triggers?.minSeverity}
|
||||
value={query.options?.minSeverity}
|
||||
options={severityOptions}
|
||||
onChange={onMinSeverityChange}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Count" labelWidth={12}>
|
||||
<InlineSwitch
|
||||
value={query.triggers?.count}
|
||||
onChange={() => onChange({ ...query, triggers: { ...query.triggers, count: !query.triggers?.count } })}
|
||||
value={query.options?.count}
|
||||
onChange={() => onChange({ ...query, options: { ...query.options, count: !query.options?.count } })}
|
||||
/>
|
||||
</InlineField>
|
||||
</QueryEditorRow>
|
||||
|
||||
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_TRIGGERS = '4';
|
||||
export const MODE_PROBLEMS = '5';
|
||||
export const MODE_MACROS = '6';
|
||||
|
||||
// Triggers severity
|
||||
export const SEV_NOT_CLASSIFIED = 0;
|
||||
|
||||
@@ -241,10 +241,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return this.queryITServiceData(target, timeRange, request);
|
||||
} else if (target.queryType === c.MODE_TRIGGERS) {
|
||||
// Triggers query
|
||||
return this.queryTriggersData(target, timeRange);
|
||||
return this.queryTriggersData(target, timeRange, request);
|
||||
} else if (target.queryType === c.MODE_PROBLEMS) {
|
||||
// Problems query
|
||||
return this.queryProblems(target, timeRange, request);
|
||||
} else if (target.queryType === c.MODE_MACROS) {
|
||||
// UserMacro query
|
||||
return this.queryUserMacrosData(target);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -254,7 +257,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return Promise.all(_.flatten(promises))
|
||||
.then(_.flatten)
|
||||
.then((data) => {
|
||||
if (data && data.length > 0 && isDataFrame(data[0]) && !utils.isProblemsDataFrame(data[0])) {
|
||||
if (
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
isDataFrame(data[0]) &&
|
||||
!utils.isProblemsDataFrame(data[0]) &&
|
||||
!utils.isMacrosDataFrame(data[0]) &&
|
||||
!utils.nonTimeSeriesDataFrame(data[0])
|
||||
) {
|
||||
data = responseHandler.alignFrames(data);
|
||||
if (responseHandler.isConvertibleToWide(data)) {
|
||||
console.log('Converting response to the wide format');
|
||||
@@ -504,31 +514,120 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
return this.handleBackendPostProcessingResponse(processedResponse, request, target);
|
||||
}
|
||||
|
||||
queryTriggersData(target, timeRange) {
|
||||
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,
|
||||
};
|
||||
async queryUserMacrosData(target) {
|
||||
const groupFilter = target.group.filter;
|
||||
return Promise.all([
|
||||
this.zabbix.getHostAlerts(hostids, appids, options),
|
||||
this.zabbix.getGroups(groupFilter),
|
||||
]).then(([triggers, groups]) => {
|
||||
return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
|
||||
});
|
||||
} else {
|
||||
const hostFilter = target.host.filter;
|
||||
const macroFilter = target.macro.filter;
|
||||
const macros = await this.zabbix.getUMacros(groupFilter, hostFilter, macroFilter);
|
||||
const hostmacroids = _.map(macros, 'hostmacroid');
|
||||
const userMacros = await this.zabbix.getUserMacros(hostmacroids);
|
||||
return responseHandler.handleMacro(userMacros, target);
|
||||
}
|
||||
|
||||
async queryTriggersData(target: ZabbixMetricsQuery, timeRange, request) {
|
||||
if (target.countTriggersBy === 'items') {
|
||||
return this.queryTriggersICData(target, timeRange);
|
||||
} else if (target.countTriggersBy === 'problems') {
|
||||
return this.queryTriggersPCData(target, timeRange, request);
|
||||
}
|
||||
|
||||
const [hosts, apps] = await this.zabbix.getHostsApsFromTarget(target);
|
||||
if (!hosts.length) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const groupFilter = target.group.filter;
|
||||
const groups = await this.zabbix.getGroups(groupFilter);
|
||||
|
||||
const hostids = hosts?.map((h) => h.hostid);
|
||||
const appids = apps?.map((a) => a.applicationid);
|
||||
const options = getTriggersOptions(target, timeRange);
|
||||
const alerts = await this.zabbix.getHostAlerts(hostids, appids, options);
|
||||
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
|
||||
}
|
||||
|
||||
async queryTriggersICData(target, timeRange) {
|
||||
const getItemOptions = { itemtype: 'num' };
|
||||
const [hosts, apps, items] = await this.zabbix.getHostsFromICTarget(target, getItemOptions);
|
||||
if (!hosts.length) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const groupFilter = target.group.filter;
|
||||
const groups = await this.zabbix.getGroups(groupFilter);
|
||||
|
||||
const hostids = hosts?.map((h) => h.hostid);
|
||||
const appids = apps?.map((a) => a.applicationid);
|
||||
const itemids = items?.map((i) => i.itemid);
|
||||
if (!itemids.length) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const options = getTriggersOptions(target, timeRange);
|
||||
const alerts = await this.zabbix.getHostICAlerts(hostids, appids, itemids, options);
|
||||
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
|
||||
}
|
||||
|
||||
async queryTriggersPCData(target: ZabbixMetricsQuery, timeRange, request) {
|
||||
const [timeFrom, timeTo] = timeRange;
|
||||
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, request.scopedVars);
|
||||
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
||||
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', '');
|
||||
const tags = utils.parseTags(tagsFilterStr);
|
||||
tags.forEach((tag) => {
|
||||
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
|
||||
tag.operator = 1;
|
||||
});
|
||||
|
||||
const problemsOptions: any = {
|
||||
minSeverity: target.options?.minSeverity,
|
||||
limit: target.options?.limit,
|
||||
};
|
||||
|
||||
if (tags && tags.length) {
|
||||
problemsOptions.tags = tags;
|
||||
}
|
||||
|
||||
if (target.options?.acknowledged === 0 || target.options?.acknowledged === 1) {
|
||||
problemsOptions.acknowledged = target.options?.acknowledged ? true : false;
|
||||
}
|
||||
|
||||
if (target.options?.minSeverity) {
|
||||
let severities = [0, 1, 2, 3, 4, 5].filter((v) => v >= target.options?.minSeverity);
|
||||
if (target.options?.severities) {
|
||||
severities = severities.filter((v) => target.options?.severities.includes(v));
|
||||
}
|
||||
problemsOptions.severities = severities;
|
||||
}
|
||||
|
||||
if (target.options.useTimeRange) {
|
||||
problemsOptions.timeFrom = timeFrom;
|
||||
problemsOptions.timeTo = timeTo;
|
||||
}
|
||||
|
||||
const [hosts, apps, triggers] = await this.zabbix.getHostsFromPCTarget(target, problemsOptions);
|
||||
if (!hosts.length) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const groupFilter = target.group.filter;
|
||||
const groups = await this.zabbix.getGroups(groupFilter);
|
||||
|
||||
const hostids = hosts?.map((h) => h.hostid);
|
||||
const appids = apps?.map((a) => a.applicationid);
|
||||
const triggerids = triggers.map((t) => t.triggerid);
|
||||
const options: any = {
|
||||
minSeverity: target.options?.minSeverity,
|
||||
acknowledged: target.options?.acknowledged,
|
||||
count: target.options.count,
|
||||
};
|
||||
if (target.options.useTimeRange) {
|
||||
options.timeFrom = timeFrom;
|
||||
options.timeTo = timeTo;
|
||||
}
|
||||
|
||||
const alerts = await this.zabbix.getHostPCAlerts(hostids, appids, triggerids, options);
|
||||
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
|
||||
}
|
||||
|
||||
queryProblems(target: ZabbixMetricsQuery, timeRange, options) {
|
||||
@@ -742,6 +841,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
|
||||
templateSrv.variableExists(target.application?.filter) ||
|
||||
templateSrv.variableExists(target.itemTag?.filter) ||
|
||||
templateSrv.variableExists(target.item?.filter) ||
|
||||
templateSrv.variableExists(target.macro?.filter) ||
|
||||
templateSrv.variableExists(target.proxy?.filter) ||
|
||||
templateSrv.variableExists(target.trigger?.filter) ||
|
||||
templateSrv.variableExists(target.textFilter) ||
|
||||
@@ -972,3 +1072,17 @@ function getRequestTarget(request: DataQueryRequest<any>, refId: string): any {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const getTriggersOptions = (target: ZabbixMetricsQuery, timeRange) => {
|
||||
const [timeFrom, timeTo] = timeRange;
|
||||
const options: any = {
|
||||
minSeverity: target.options?.minSeverity,
|
||||
acknowledged: target.options?.acknowledged,
|
||||
count: target.options?.count,
|
||||
};
|
||||
if (target.options?.useTimeRange) {
|
||||
options.timeFrom = timeFrom;
|
||||
options.timeTo = timeTo;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@ import _ from 'lodash';
|
||||
import { ZabbixMetricsQuery } from './types';
|
||||
import * as c from './constants';
|
||||
|
||||
export const DS_QUERY_SCHEMA = 11;
|
||||
export const DS_CONFIG_SCHEMA = 3;
|
||||
|
||||
/**
|
||||
* Query format migration.
|
||||
* This module can detect query format version and make migration.
|
||||
@@ -28,6 +31,7 @@ export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
|
||||
target.host.filter = target.host.name === '*' ? convertToRegex(target.hostFilter) : target.host.name;
|
||||
target.application.filter = target.application.name === '*' ? '' : target.application.name;
|
||||
target.item.filter = target.item.name === 'All' ? convertToRegex(target.itemFilter) : target.item.name;
|
||||
target.macro.filter = target.macro.macro === '*' ? convertToRegex(target.macroFilter) : target.macro.macro;
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -85,6 +89,32 @@ function migrateSLAProperty(target) {
|
||||
}
|
||||
}
|
||||
|
||||
function migrateTriggersMode(target: any) {
|
||||
if (target.triggers?.minSeverity) {
|
||||
target.options.minSeverity = target.triggers?.minSeverity;
|
||||
delete target.triggers.minSeverity;
|
||||
}
|
||||
if (target.triggers?.count) {
|
||||
target.options.count = target.triggers?.count;
|
||||
delete target.triggers.count;
|
||||
}
|
||||
}
|
||||
|
||||
function migrateNewTriggersCountModes(target: any) {
|
||||
if (target.schema >= 11) {
|
||||
return;
|
||||
}
|
||||
if (target.queryType === '6') {
|
||||
target.queryType = c.MODE_TRIGGERS;
|
||||
target.countTriggersBy = 'items';
|
||||
} else if (target.queryType === '7') {
|
||||
target.queryType = c.MODE_TRIGGERS;
|
||||
target.countTriggersBy = 'problems';
|
||||
} else if (target.queryType === '8') {
|
||||
target.queryType = c.MODE_MACROS;
|
||||
}
|
||||
}
|
||||
|
||||
export function migrate(target) {
|
||||
target.resultFormat = target.resultFormat || 'time_series';
|
||||
target = fixTargetGroup(target);
|
||||
@@ -97,6 +127,10 @@ export function migrate(target) {
|
||||
migrateProblemSort(target);
|
||||
migrateApplications(target);
|
||||
migrateSLAProperty(target);
|
||||
migrateTriggersMode(target);
|
||||
migrateNewTriggersCountModes(target);
|
||||
|
||||
target.schema = DS_QUERY_SCHEMA;
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -115,8 +149,6 @@ function convertToRegex(str) {
|
||||
}
|
||||
}
|
||||
|
||||
export const DS_CONFIG_SCHEMA = 3;
|
||||
|
||||
export function migrateDSConfig(jsonData) {
|
||||
if (!jsonData) {
|
||||
jsonData = {};
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
TIME_SERIES_VALUE_FIELD_NAME,
|
||||
} from '@grafana/data';
|
||||
import { ZabbixMetricsQuery } from './types';
|
||||
import { ZabbixMetricsQuery, ZBXGroup, ZBXTrigger } from './types';
|
||||
|
||||
/**
|
||||
* Convert Zabbix API history.get response to Grafana format
|
||||
@@ -74,6 +74,29 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleMacro(macros, target): MutableDataFrame {
|
||||
const frame = new MutableDataFrame({
|
||||
refId: target.refId,
|
||||
name: 'macros',
|
||||
fields: [
|
||||
{ name: 'Host', type: FieldType.string },
|
||||
{ name: 'Macros', type: FieldType.string },
|
||||
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.string },
|
||||
],
|
||||
});
|
||||
|
||||
for (let i = 0; i < macros.length; i++) {
|
||||
const m = macros[i];
|
||||
const dataRow: any = {
|
||||
Host: m.hosts[0]!.name,
|
||||
Macros: m.macro,
|
||||
[TIME_SERIES_VALUE_FIELD_NAME]: m.value,
|
||||
};
|
||||
frame.add(dataRow);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
export function seriesToDataFrame(
|
||||
timeseries,
|
||||
target: ZabbixMetricsQuery,
|
||||
@@ -589,7 +612,7 @@ export function handleSLAResponse(itservice, slaProperty, slaObject) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleTriggersResponse(triggers, groups, timeRange) {
|
||||
function handleTriggersResponse(triggers: ZBXTrigger[], groups: ZBXGroup[], timeRange: number[], target) {
|
||||
if (!_.isArray(triggers)) {
|
||||
let triggersCount = null;
|
||||
try {
|
||||
@@ -597,29 +620,47 @@ function handleTriggersResponse(triggers, groups, timeRange) {
|
||||
} catch (err) {
|
||||
console.log('Error when handling triggers count: ', err);
|
||||
}
|
||||
return {
|
||||
target: 'triggers count',
|
||||
datapoints: [[triggersCount, timeRange[1] * 1000]],
|
||||
};
|
||||
|
||||
const frame = new MutableDataFrame({
|
||||
refId: target.refId,
|
||||
fields: [
|
||||
{ name: TIME_SERIES_TIME_FIELD_NAME, type: FieldType.time, values: new ArrayVector([timeRange[1] * 1000]) },
|
||||
{ name: TIME_SERIES_VALUE_FIELD_NAME, type: FieldType.number, values: new ArrayVector([triggersCount]) },
|
||||
],
|
||||
length: 1,
|
||||
});
|
||||
|
||||
return frame;
|
||||
} else {
|
||||
const stats = getTriggerStats(triggers);
|
||||
const groupNames = _.map(groups, 'name');
|
||||
const table: any = new TableModel();
|
||||
table.addColumn({ text: 'Host group' });
|
||||
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
|
||||
table.addColumn({ text: severity.text });
|
||||
const frame = new MutableDataFrame({
|
||||
refId: target.refId,
|
||||
fields: [{ name: 'Host group', type: FieldType.string, values: new ArrayVector() }],
|
||||
});
|
||||
|
||||
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,
|
||||
handleTrends,
|
||||
handleText,
|
||||
handleMacro,
|
||||
handleHistoryAsTable,
|
||||
handleSLAResponse,
|
||||
handleTriggersResponse,
|
||||
|
||||
@@ -34,17 +34,19 @@ export interface ZabbixConnectionTestQuery {
|
||||
}
|
||||
|
||||
export interface ZabbixMetricsQuery extends DataQuery {
|
||||
schema: number;
|
||||
queryType: string;
|
||||
datasourceId?: number;
|
||||
group?: { filter: string; name?: string };
|
||||
host?: { filter: string; name?: string };
|
||||
application?: { filter: string; name?: string };
|
||||
itemTag?: { filter: string; name?: string };
|
||||
item?: { filter: string; name?: string };
|
||||
textFilter?: string;
|
||||
mode?: number;
|
||||
itemids?: string;
|
||||
useCaptureGroups?: boolean;
|
||||
datasourceId: number;
|
||||
group: { filter: string; name?: string };
|
||||
host: { filter: string; name?: string };
|
||||
application: { filter: string; name?: string };
|
||||
itemTag: { filter: string; name?: string };
|
||||
item: { filter: string; name?: string };
|
||||
macro: { filter: string; macro?: string };
|
||||
textFilter: string;
|
||||
mode: number;
|
||||
itemids: string;
|
||||
useCaptureGroups: boolean;
|
||||
proxy?: { filter: string };
|
||||
trigger?: { filter: string };
|
||||
itServiceFilter?: string;
|
||||
@@ -53,6 +55,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
|
||||
slaInterval?: string;
|
||||
tags?: { filter: string };
|
||||
triggers?: { minSeverity: number; acknowledged: number; count: boolean };
|
||||
countTriggersBy?: 'problems' | 'items' | '';
|
||||
functions?: MetricFunc[];
|
||||
options?: ZabbixQueryOptions;
|
||||
// Problems
|
||||
@@ -60,6 +63,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
|
||||
// Deprecated
|
||||
hostFilter?: string;
|
||||
itemFilter?: string;
|
||||
macroFilter?: string;
|
||||
}
|
||||
|
||||
export interface ZabbixQueryOptions {
|
||||
@@ -77,6 +81,7 @@ export interface ZabbixQueryOptions {
|
||||
limit?: number;
|
||||
useTimeRange?: boolean;
|
||||
severities?: number[];
|
||||
count?: boolean;
|
||||
|
||||
// Annotations
|
||||
showOkEvents?: boolean;
|
||||
@@ -186,6 +191,7 @@ export interface VariableQuery {
|
||||
application?: string;
|
||||
itemTag?: string;
|
||||
item?: string;
|
||||
macro?: string;
|
||||
}
|
||||
|
||||
export type LegacyVariableQuery = VariableQuery | string;
|
||||
@@ -194,6 +200,7 @@ export enum VariableQueryTypes {
|
||||
Group = 'group',
|
||||
Host = 'host',
|
||||
Application = 'application',
|
||||
Macro = 'macro',
|
||||
ItemTag = 'itemTag',
|
||||
Item = 'item',
|
||||
ItemValues = 'itemValues',
|
||||
@@ -330,6 +337,7 @@ export interface ZBXHost {
|
||||
maintenance_status?: string;
|
||||
proxy_hostid?: string;
|
||||
proxy?: any;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ZBXItem {
|
||||
@@ -340,6 +348,13 @@ export interface ZBXItem {
|
||||
tags?: ZBXItemTag[];
|
||||
}
|
||||
|
||||
export interface ZBXApp {
|
||||
applicationid: string;
|
||||
hostid: string;
|
||||
name: string;
|
||||
templateids?: string;
|
||||
}
|
||||
|
||||
export interface ZBXItemTag {
|
||||
tag: string;
|
||||
value?: string;
|
||||
|
||||
@@ -3,7 +3,15 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import * as c from './constants';
|
||||
import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types';
|
||||
import { DataFrame, FieldType, getValueFormats, MappingType, rangeUtil, ValueMapping } from '@grafana/data';
|
||||
import {
|
||||
DataFrame,
|
||||
FieldType,
|
||||
getValueFormats,
|
||||
MappingType,
|
||||
rangeUtil,
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
ValueMapping,
|
||||
} from '@grafana/data';
|
||||
|
||||
/*
|
||||
* This regex matches 3 types of variable reference with an optional format specifier
|
||||
@@ -507,6 +515,14 @@ export function isProblemsDataFrame(data: DataFrame): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isMacrosDataFrame(data: DataFrame): boolean {
|
||||
return data.name === 'macros';
|
||||
}
|
||||
|
||||
export function nonTimeSeriesDataFrame(data: DataFrame): boolean {
|
||||
return !data.fields.find((f) => f.type === FieldType.time || f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
}
|
||||
|
||||
// Swap n and k elements.
|
||||
export function swap<T>(list: T[], n: number, k: number): T[] {
|
||||
if (list === null || list.length < 2 || k > list.length - 1 || k < 0 || n > list.length - 1 || n < 0) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import semver from 'semver';
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import * as utils from '../../../utils';
|
||||
import { MIN_SLA_INTERVAL, ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_NONE } from '../../../constants';
|
||||
import { ShowProblemTypes, ZBXProblem } from '../../../types';
|
||||
import { ShowProblemTypes, ZBXProblem, ZBXTrigger } from '../../../types';
|
||||
import { APIExecuteScriptResponse, JSONRPCError, ZBXScript } from './types';
|
||||
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
@@ -228,6 +228,15 @@ export class ZabbixAPIConnector {
|
||||
return this.request('usermacro.get', params);
|
||||
}
|
||||
|
||||
getUserMacros(hostmacroids) {
|
||||
const params = {
|
||||
output: 'extend',
|
||||
hostmacroids: hostmacroids,
|
||||
selectHosts: ['hostid', 'name'],
|
||||
};
|
||||
return this.request('usermacro.get', params);
|
||||
}
|
||||
|
||||
getGlobalMacros() {
|
||||
const params = {
|
||||
output: 'extend',
|
||||
@@ -506,10 +515,11 @@ export class ZabbixAPIConnector {
|
||||
expandDescription: true,
|
||||
expandData: true,
|
||||
expandComment: true,
|
||||
expandExpression: true,
|
||||
monitored: true,
|
||||
skipDependent: true,
|
||||
selectGroups: ['name', 'groupid'],
|
||||
selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid'],
|
||||
selectHosts: ['hostid', 'name', 'host', 'maintenance_status', 'proxy_hostid', 'description'],
|
||||
selectItems: ['itemid', 'name', 'key_', 'lastvalue'],
|
||||
// selectLastEvent: 'extend',
|
||||
// selectTags: 'extend',
|
||||
@@ -680,7 +690,7 @@ export class ZabbixAPIConnector {
|
||||
return this.request('trigger.get', params);
|
||||
}
|
||||
|
||||
getHostAlerts(hostids, applicationids, options) {
|
||||
async getHostAlerts(hostids, applicationids, options): Promise<ZBXTrigger[]> {
|
||||
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
|
||||
const params: any = {
|
||||
output: 'extend',
|
||||
@@ -697,6 +707,100 @@ export class ZabbixAPIConnector {
|
||||
selectHosts: ['hostid', 'host', 'name'],
|
||||
};
|
||||
|
||||
if (count && acknowledged !== 1) {
|
||||
params.countOutput = true;
|
||||
if (acknowledged === 0) {
|
||||
params.withLastEventUnacknowledged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (applicationids && applicationids.length) {
|
||||
params.applicationids = applicationids;
|
||||
}
|
||||
|
||||
if (timeFrom || timeTo) {
|
||||
params.lastChangeSince = timeFrom;
|
||||
params.lastChangeTill = timeTo;
|
||||
}
|
||||
|
||||
let triggers = await this.request('trigger.get', params);
|
||||
if (!count || acknowledged === 1) {
|
||||
triggers = filterTriggersByAcknowledge(triggers, acknowledged);
|
||||
if (count) {
|
||||
triggers = triggers.length;
|
||||
}
|
||||
}
|
||||
return triggers;
|
||||
}
|
||||
|
||||
getHostICAlerts(hostids, applicationids, itemids, options) {
|
||||
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
|
||||
const params: any = {
|
||||
output: 'extend',
|
||||
hostids: hostids,
|
||||
min_severity: minSeverity,
|
||||
filter: { value: 1 },
|
||||
expandDescription: true,
|
||||
expandData: true,
|
||||
expandComment: true,
|
||||
monitored: true,
|
||||
skipDependent: true,
|
||||
selectLastEvent: 'extend',
|
||||
selectGroups: 'extend',
|
||||
selectHosts: ['host', 'name'],
|
||||
selectItems: ['name', 'key_'],
|
||||
};
|
||||
|
||||
if (count && acknowledged !== 1) {
|
||||
params.countOutput = true;
|
||||
if (acknowledged === 0) {
|
||||
params.withLastEventUnacknowledged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (applicationids && applicationids.length) {
|
||||
params.applicationids = applicationids;
|
||||
}
|
||||
|
||||
if (itemids && itemids.length) {
|
||||
params.itemids = itemids;
|
||||
}
|
||||
|
||||
if (timeFrom || timeTo) {
|
||||
params.lastChangeSince = timeFrom;
|
||||
params.lastChangeTill = timeTo;
|
||||
}
|
||||
|
||||
return this.request('trigger.get', params).then((triggers) => {
|
||||
if (!count || acknowledged === 1) {
|
||||
triggers = filterTriggersByAcknowledge(triggers, acknowledged);
|
||||
if (count) {
|
||||
triggers = triggers.length;
|
||||
}
|
||||
}
|
||||
return triggers;
|
||||
});
|
||||
}
|
||||
|
||||
getHostPCAlerts(hostids, applicationids, triggerids, options) {
|
||||
const { minSeverity, acknowledged, count, timeFrom, timeTo } = options;
|
||||
const params: any = {
|
||||
output: 'extend',
|
||||
hostids: hostids,
|
||||
triggerids: triggerids,
|
||||
min_severity: minSeverity,
|
||||
filter: { value: 1 },
|
||||
expandDescription: true,
|
||||
expandData: true,
|
||||
expandComment: true,
|
||||
monitored: true,
|
||||
skipDependent: true,
|
||||
selectLastEvent: 'extend',
|
||||
selectGroups: 'extend',
|
||||
selectHosts: ['host', 'name'],
|
||||
selectItems: ['name', 'key_'],
|
||||
};
|
||||
|
||||
if (count && acknowledged !== 0 && acknowledged !== 1) {
|
||||
params.countOutput = true;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,22 @@ export interface ZabbixConnector {
|
||||
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
|
||||
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
|
||||
getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
|
||||
getHostICAlerts: (hostids, applicationids, itemids, options?) => Promise<any>;
|
||||
getHostPCAlerts: (hostids, applicationids, triggerids, options?) => Promise<any>;
|
||||
getAcknowledges: (eventids) => Promise<any>;
|
||||
getITService: (serviceids?) => Promise<any>;
|
||||
acknowledgeEvent: (eventid, message) => Promise<any>;
|
||||
getProxies: () => Promise<any>;
|
||||
getEventAlerts: (eventids) => Promise<any>;
|
||||
getExtendedEventData: (eventids) => Promise<any>;
|
||||
getUserMacros: (hostmacroids) => Promise<any>;
|
||||
getMacros: (hostids: any[]) => Promise<any>;
|
||||
getVersion: () => Promise<string>;
|
||||
|
||||
getGroups: (groupFilter?) => any;
|
||||
getHosts: (groupFilter?, hostFilter?) => any;
|
||||
getApps: (groupFilter?, hostFilter?, appFilter?) => any;
|
||||
getUMacros: (groupFilter?, hostFilter?, macroFilter?) => any;
|
||||
getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options?) => any;
|
||||
getSLA: (itservices, timeRange, target, options?) => any;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SQLConnector } from './connectors/sql/sqlConnector';
|
||||
import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector';
|
||||
import { ZabbixConnector } from './types';
|
||||
import { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler';
|
||||
import { ProblemDTO, ZabbixMetricsQuery, ZBXItem, ZBXItemTag } from '../types';
|
||||
import { ProblemDTO, ZBXApp, ZBXHost, ZBXItem, ZBXItemTag, ZBXTrigger, ZabbixMetricsQuery } from '../types';
|
||||
|
||||
interface AppsResponse extends Array<any> {
|
||||
appFilterEmpty?: boolean;
|
||||
@@ -26,13 +26,18 @@ const REQUESTS_TO_PROXYFY = [
|
||||
'getApps',
|
||||
'getItems',
|
||||
'getMacros',
|
||||
'getUMacros',
|
||||
'getItemsByIDs',
|
||||
'getEvents',
|
||||
'getAlerts',
|
||||
'getHostAlerts',
|
||||
'getUserMacros',
|
||||
'getHostICAlerts',
|
||||
'getHostPCAlerts',
|
||||
'getAcknowledges',
|
||||
'getITService',
|
||||
'getSLA',
|
||||
'getVersion',
|
||||
'getProxies',
|
||||
'getEventAlerts',
|
||||
'getExtendedEventData',
|
||||
@@ -50,6 +55,7 @@ const REQUESTS_TO_CACHE = [
|
||||
'getApps',
|
||||
'getItems',
|
||||
'getMacros',
|
||||
'getUMacros',
|
||||
'getItemsByIDs',
|
||||
'getITService',
|
||||
'getProxies',
|
||||
@@ -65,6 +71,9 @@ const REQUESTS_TO_BIND = [
|
||||
'getEvents',
|
||||
'getAlerts',
|
||||
'getHostAlerts',
|
||||
'getUserMacros',
|
||||
'getHostICAlerts',
|
||||
'getHostPCAlerts',
|
||||
'getAcknowledges',
|
||||
'getITService',
|
||||
'acknowledgeEvent',
|
||||
@@ -91,7 +100,9 @@ export class Zabbix implements ZabbixConnector {
|
||||
getItemsByIDs: (itemids) => Promise<any>;
|
||||
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
|
||||
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
|
||||
getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
|
||||
getHostAlerts: (hostids, applicationids, options?) => Promise<ZBXTrigger[]>;
|
||||
getHostICAlerts: (hostids, applicationids, itemids, options?) => Promise<any>;
|
||||
getHostPCAlerts: (hostids, applicationids, triggerids, options?) => Promise<any>;
|
||||
getAcknowledges: (eventids) => Promise<any>;
|
||||
getITService: (serviceids?) => Promise<any>;
|
||||
acknowledgeEvent: (eventid, message) => Promise<any>;
|
||||
@@ -99,6 +110,7 @@ export class Zabbix implements ZabbixConnector {
|
||||
getEventAlerts: (eventids) => Promise<any>;
|
||||
getExtendedEventData: (eventids) => Promise<any>;
|
||||
getMacros: (hostids: any[]) => Promise<any>;
|
||||
getUserMacros: (hostmacroids) => Promise<any>;
|
||||
getValueMappings: () => Promise<any>;
|
||||
getSLAList: () => Promise<any>;
|
||||
|
||||
@@ -251,17 +263,38 @@ export class Zabbix implements ZabbixConnector {
|
||||
return this.getItems(...filters, options);
|
||||
}
|
||||
|
||||
getHostsFromTarget(target) {
|
||||
const parts = ['group', 'host', 'application'];
|
||||
getMacrosFromTarget(target) {
|
||||
const parts = ['group', 'host', 'macro'];
|
||||
const filters = _.map(parts, (p) => target[p].filter);
|
||||
return Promise.all([this.getHosts(...filters), this.getApps(...filters)]).then((results) => {
|
||||
return this.getUMacros(...filters);
|
||||
}
|
||||
|
||||
async getHostsApsFromTarget(target): Promise<[ZBXHost[], ZBXApp[]]> {
|
||||
const parts = ['group', 'host', 'application'];
|
||||
const filters = parts.map((p) => target[p].filter);
|
||||
const results = await Promise.all([this.getHosts(...filters), this.getApps(...filters)]);
|
||||
const hosts = results[0];
|
||||
let apps: AppsResponse = results[1];
|
||||
if (apps.appFilterEmpty) {
|
||||
apps = [];
|
||||
}
|
||||
return [hosts, apps];
|
||||
});
|
||||
}
|
||||
|
||||
async getHostsFromICTarget(target, options): Promise<[ZBXHost[], ZBXApp[], ZBXItem[]]> {
|
||||
const parts = ['group', 'host', 'application', 'itemTag', 'item'];
|
||||
const filters = parts.map((p) => target[p].filter);
|
||||
const [hosts, apps] = await this.getHostsApsFromTarget(target);
|
||||
const items = await this.getItems(...filters, options);
|
||||
return [hosts, apps, items];
|
||||
}
|
||||
|
||||
async getHostsFromPCTarget(target, options): Promise<[ZBXHost[], ZBXApp[], ProblemDTO[]]> {
|
||||
const parts = ['group', 'host', 'application', 'proxy', 'trigger'];
|
||||
const filters = parts.map((p) => target[p].filter);
|
||||
const [hosts, apps] = await this.getHostsApsFromTarget(target);
|
||||
const problems = await this.getCProblems(...filters, options);
|
||||
return [hosts, apps, problems];
|
||||
}
|
||||
|
||||
getAllGroups() {
|
||||
@@ -318,6 +351,17 @@ export class Zabbix implements ZabbixConnector {
|
||||
});
|
||||
}
|
||||
|
||||
async getAllMacros(groupFilter, hostFilter) {
|
||||
const hosts = await this.getHosts(groupFilter, hostFilter);
|
||||
const hostids = hosts?.map((h) => h.hostid);
|
||||
return this.zabbixAPI.getMacros(hostids);
|
||||
}
|
||||
|
||||
async getUMacros(groupFilter?, hostFilter?, macroFilter?) {
|
||||
const allMacros = await this.getAllMacros(groupFilter, hostFilter);
|
||||
return filterByMQuery(allMacros, macroFilter);
|
||||
}
|
||||
|
||||
async getItemTags(groupFilter?, hostFilter?, itemTagFilter?) {
|
||||
const items = await this.getAllItems(groupFilter, hostFilter, null, null, {});
|
||||
let tags: ZBXItemTag[] = _.flatten(
|
||||
@@ -480,6 +524,43 @@ export class Zabbix implements ZabbixConnector {
|
||||
// .then(triggers => this.expandUserMacro.bind(this)(triggers, true));
|
||||
}
|
||||
|
||||
getCProblems(groupFilter?, hostFilter?, appFilter?, proxyFilter?, triggerFilter?, options?): Promise<ProblemDTO[]> {
|
||||
const promises = [
|
||||
this.getGroups(groupFilter),
|
||||
this.getHosts(groupFilter, hostFilter),
|
||||
this.getApps(groupFilter, hostFilter, appFilter),
|
||||
];
|
||||
|
||||
return Promise.all(promises)
|
||||
.then((results) => {
|
||||
const [filteredGroups, filteredHosts, filteredApps] = results;
|
||||
const query: any = {};
|
||||
|
||||
if (appFilter) {
|
||||
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
|
||||
}
|
||||
if (hostFilter && hostFilter !== '/.*/') {
|
||||
query.hostids = _.map(filteredHosts, 'hostid');
|
||||
}
|
||||
if (groupFilter) {
|
||||
query.groupids = _.map(filteredGroups, 'groupid');
|
||||
}
|
||||
|
||||
return query;
|
||||
})
|
||||
.then((query) => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options))
|
||||
.then((problems) => findByFilter(problems, triggerFilter))
|
||||
.then((problems) => {
|
||||
const triggerids = problems?.map((problem) => problem.objectid);
|
||||
return Promise.all([Promise.resolve(problems), this.zabbixAPI.getTriggersByIds(triggerids)]);
|
||||
})
|
||||
|
||||
.then(([problems, triggers]) => joinTriggersWithProblems(problems, triggers))
|
||||
.then((triggers) => this.filterTriggersByProxy(triggers, proxyFilter));
|
||||
//.then(triggers => findByFilter(triggers, triggerFilter));
|
||||
// .then(triggers => this.expandUserMacro.bind(this)(triggers, true));
|
||||
}
|
||||
|
||||
filterTriggersByProxy(triggers, proxyFilter) {
|
||||
return this.getFilteredProxies(proxyFilter).then((proxies) => {
|
||||
if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
|
||||
@@ -612,6 +693,15 @@ function filterByName(list, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function filterByMacro(list, name) {
|
||||
const finded = _.filter(list, { macro: name });
|
||||
if (finded) {
|
||||
return finded;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function filterByRegex(list, regex) {
|
||||
const filterPattern = utils.buildRegex(regex);
|
||||
return _.filter(list, (zbx_obj) => {
|
||||
@@ -619,6 +709,13 @@ function filterByRegex(list, regex) {
|
||||
});
|
||||
}
|
||||
|
||||
function filterByMRegex(list, regex) {
|
||||
const filterPattern = utils.buildRegex(regex);
|
||||
return _.filter(list, (zbx_obj) => {
|
||||
return filterPattern.test(zbx_obj?.macro);
|
||||
});
|
||||
}
|
||||
|
||||
function findByFilter(list, filter) {
|
||||
if (utils.isRegex(filter)) {
|
||||
return filterByRegex(list, filter);
|
||||
@@ -635,6 +732,14 @@ function filterByQuery(list, filter) {
|
||||
}
|
||||
}
|
||||
|
||||
function filterByMQuery(list, filter) {
|
||||
if (utils.isRegex(filter)) {
|
||||
return filterByMRegex(list, filter);
|
||||
} else {
|
||||
return filterByMacro(list, filter);
|
||||
}
|
||||
}
|
||||
|
||||
function getHostIds(items) {
|
||||
const hostIds = _.map(items, (item) => {
|
||||
return _.map(item.hosts, 'hostid');
|
||||
|
||||
@@ -6,7 +6,7 @@ import moment from 'moment';
|
||||
import { isNewProblem, formatLastChange } from '../../utils';
|
||||
import { ProblemsPanelOptions, TriggerSeverity } from '../../types';
|
||||
import { AckProblemData, AckModal } from '../AckModal';
|
||||
import EventTag from '../EventTag';
|
||||
import { EventTag } from '../EventTag';
|
||||
import AlertAcknowledges from './AlertAcknowledges';
|
||||
import AlertIcon from './AlertIcon';
|
||||
import { ProblemDTO, ZBXTag } from '../../../datasource/types';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DataSourceRef } from '@grafana/data';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||
import { ZBXTag } from '../../datasource/types';
|
||||
|
||||
const TAG_COLORS = [
|
||||
@@ -85,39 +87,58 @@ function djb2(str) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
interface EventTagProps {
|
||||
const URLPattern = /^https?:\/\/.+/;
|
||||
|
||||
interface Props {
|
||||
tag: ZBXTag;
|
||||
datasource: DataSourceRef | string;
|
||||
highlight?: boolean;
|
||||
onClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export default class EventTag extends PureComponent<EventTagProps> {
|
||||
handleClick = (event) => {
|
||||
if (this.props.onClick) {
|
||||
const { tag, datasource } = this.props;
|
||||
this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey);
|
||||
export const EventTag = ({ tag, datasource, highlight, onClick }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const onClickInternal = (event) => {
|
||||
if (onClick) {
|
||||
onClick(tag, datasource, event.ctrlKey, event.shiftKey);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tag, highlight } = this.props;
|
||||
const tagColor = getTagColorsFromName(tag.tag);
|
||||
const style: React.CSSProperties = {
|
||||
background: tagColor.color,
|
||||
borderColor: tagColor.borderColor,
|
||||
};
|
||||
|
||||
const isUrl = URLPattern.test(tag.value);
|
||||
let tagElement = <>{tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`}</>;
|
||||
if (isUrl) {
|
||||
tagElement = (
|
||||
<Tooltip placement="top" content={tag.value}>
|
||||
<a href={tag.value} target="_blank" rel="noreferrer">
|
||||
<Icon name="link" className={styles.icon} />
|
||||
{tag.tag}
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// TODO: show tooltip when click feature is fixed
|
||||
// <Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove">
|
||||
<span
|
||||
className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`}
|
||||
style={style}
|
||||
onClick={this.handleClick}
|
||||
onClick={onClickInternal}
|
||||
>
|
||||
{tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`}
|
||||
{tagElement}
|
||||
</span>
|
||||
// </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';
|
||||
|
||||
type Props = StandardEditorProps<TriggerSeverity[]>;
|
||||
|
||||
export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => {
|
||||
const onSeverityItemChange = (severity: TriggerSeverity) => {
|
||||
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
|
||||
import moment from 'moment';
|
||||
import { TimeRange, DataSourceRef } from '@grafana/data';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { TimeRange, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import * as utils from '../../../datasource/utils';
|
||||
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag, ZBXItem } from '../../../datasource/types';
|
||||
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource/types';
|
||||
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types';
|
||||
import { AckModal, AckProblemData } from '../AckModal';
|
||||
import EventTag from '../EventTag';
|
||||
import { EventTag } from '../EventTag';
|
||||
import AcknowledgesList from './AcknowledgesList';
|
||||
import ProblemTimeline from './ProblemTimeline';
|
||||
import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components';
|
||||
import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal';
|
||||
import ProblemStatusBar from './ProblemStatusBar';
|
||||
import { RTRow } from '../../types';
|
||||
import { ProblemItems } from './ProblemItems';
|
||||
import { ProblemHosts, ProblemHostsDescription } from './ProblemHosts';
|
||||
import { ProblemGroups } from './ProblemGroups';
|
||||
import { ProblemExpression } from './ProblemExpression';
|
||||
|
||||
interface ProblemDetailsProps extends RTRow<ProblemDTO> {
|
||||
interface Props extends RTRow<ProblemDTO> {
|
||||
rootWidth: number;
|
||||
timeRange: TimeRange;
|
||||
showTimeline?: boolean;
|
||||
@@ -24,95 +28,90 @@ interface ProblemDetailsProps extends RTRow<ProblemDTO> {
|
||||
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
|
||||
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
|
||||
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
|
||||
|
||||
onExecuteScript(problem: ProblemDTO, scriptid: string): Promise<APIExecuteScriptResponse>;
|
||||
|
||||
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
|
||||
onTagClick?: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
}
|
||||
|
||||
interface ProblemDetailsState {
|
||||
events: ZBXEvent[];
|
||||
alerts: ZBXAlert[];
|
||||
show: boolean;
|
||||
}
|
||||
export const ProblemDetails = ({
|
||||
original,
|
||||
rootWidth,
|
||||
timeRange,
|
||||
showTimeline,
|
||||
panelId,
|
||||
getProblemAlerts,
|
||||
getProblemEvents,
|
||||
getScripts,
|
||||
onExecuteScript,
|
||||
onProblemAck,
|
||||
onTagClick,
|
||||
}: Props) => {
|
||||
const [events, setEvents] = useState([]);
|
||||
const [alerts, setAletrs] = useState([]);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
events: [],
|
||||
alerts: [],
|
||||
show: false,
|
||||
};
|
||||
useEffect(() => {
|
||||
if (showTimeline) {
|
||||
fetchProblemEvents();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.showTimeline) {
|
||||
this.fetchProblemEvents();
|
||||
}
|
||||
this.fetchProblemAlerts();
|
||||
fetchProblemAlerts();
|
||||
requestAnimationFrame(() => {
|
||||
this.setState({ show: true });
|
||||
setShow(true);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
|
||||
const fetchProblemEvents = async () => {
|
||||
const problem = original;
|
||||
const events = await getProblemEvents(problem);
|
||||
setEvents(events);
|
||||
};
|
||||
|
||||
const fetchProblemAlerts = async () => {
|
||||
const problem = original;
|
||||
const alerts = await getProblemAlerts(problem);
|
||||
setAletrs(alerts);
|
||||
};
|
||||
|
||||
const handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (onTagClick) {
|
||||
onTagClick(tag, datasource, ctrlKey, shiftKey);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProblemEvents() {
|
||||
const problem = this.props.original;
|
||||
this.props.getProblemEvents(problem).then((events) => {
|
||||
this.setState({ events });
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
const ackProblem = (data: AckProblemData) => {
|
||||
const problem = original as ProblemDTO;
|
||||
return onProblemAck(problem, data);
|
||||
};
|
||||
|
||||
getScripts = () => {
|
||||
const problem = this.props.original as ProblemDTO;
|
||||
return this.props.getScripts(problem);
|
||||
const getScriptsInternal = () => {
|
||||
const problem = original as ProblemDTO;
|
||||
return getScripts(problem);
|
||||
};
|
||||
|
||||
onExecuteScript = (data: ExecScriptData) => {
|
||||
const problem = this.props.original as ProblemDTO;
|
||||
return this.props.onExecuteScript(problem, data.scriptid);
|
||||
const onExecuteScriptInternal = (data: ExecScriptData) => {
|
||||
const problem = original as ProblemDTO;
|
||||
return onExecuteScript(problem, data.scriptid);
|
||||
};
|
||||
|
||||
render() {
|
||||
const problem = this.props.original as ProblemDTO;
|
||||
const alerts = this.state.alerts;
|
||||
const { rootWidth, panelId, timeRange } = this.props;
|
||||
const displayClass = this.state.show ? 'show' : '';
|
||||
const problem = original as ProblemDTO;
|
||||
const displayClass = show ? 'show' : '';
|
||||
const wideLayout = rootWidth > 1200;
|
||||
const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400);
|
||||
const age = moment.unix(problem.timestamp).fromNow(true);
|
||||
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
|
||||
const problemSeverity = Number(problem.severity);
|
||||
|
||||
let dsName: string = this.props.original.datasource as string;
|
||||
if ((this.props.original.datasource as DataSourceRef)?.uid) {
|
||||
const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid);
|
||||
let dsName: string = original.datasource as string;
|
||||
if ((original.datasource as DataSourceRef)?.uid) {
|
||||
const dsInstance = getDataSourceSrv().getInstanceSettings((original.datasource as DataSourceRef).uid);
|
||||
dsName = dsInstance.name;
|
||||
}
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={`problem-details-container ${displayClass}`}>
|
||||
<div className="problem-details-body">
|
||||
<div className="problem-details">
|
||||
<div className={styles.problemDetails}>
|
||||
<div className="problem-details-head">
|
||||
<div className="problem-actions-left">
|
||||
<ExploreButton problem={problem} panelId={panelId} range={timeRange} />
|
||||
@@ -125,8 +124,8 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
className="problem-action-button"
|
||||
onClick={() => {
|
||||
showModal(ExecScriptModal, {
|
||||
getScripts: this.getScripts,
|
||||
onSubmit: this.onExecuteScript,
|
||||
getScripts: getScriptsInternal,
|
||||
onSubmit: onExecuteScriptInternal,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
@@ -141,7 +140,7 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
showModal(AckModal, {
|
||||
canClose: problem.manual_close === '1',
|
||||
severity: problemSeverity,
|
||||
onSubmit: this.ackProblem,
|
||||
onSubmit: ackProblem,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
@@ -164,13 +163,24 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
{problem.comments && (
|
||||
<div className="problem-description-row">
|
||||
<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>
|
||||
</Tooltip>
|
||||
<span>{problem.comments}</span>
|
||||
{/* <span>{problem.comments}</span> */}
|
||||
<span dangerouslySetInnerHTML={{ __html: problem.comments }} />
|
||||
</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 && (
|
||||
<div className="problem-tags">
|
||||
{problem.tags &&
|
||||
@@ -180,14 +190,12 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
tag={tag}
|
||||
datasource={problem.datasource}
|
||||
highlight={tag.tag === problem.correlation_tag}
|
||||
onClick={this.handleTagClick}
|
||||
onClick={handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{this.props.showTimeline && this.state.events.length > 0 && (
|
||||
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange} />
|
||||
)}
|
||||
{showTimeline && events.length > 0 && <ProblemTimeline events={events} timeRange={timeRange} />}
|
||||
{showAcknowledges && !wideLayout && (
|
||||
<div className="problem-ack-container">
|
||||
<h6>
|
||||
@@ -218,86 +226,22 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
<span>{problem.proxy}</span>
|
||||
</div>
|
||||
)}
|
||||
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
|
||||
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
|
||||
{problem.groups && <ProblemGroups groups={problem.groups} />}
|
||||
{problem.hosts && <ProblemHosts hosts={problem.hosts} />}
|
||||
</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 {
|
||||
groups: ZBXGroup[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class ProblemGroups extends PureComponent<ProblemGroupsProps> {
|
||||
render() {
|
||||
return this.props.groups.map((g) => (
|
||||
<div className={this.props.className || ''} key={g.groupid}>
|
||||
<FAIcon icon="folder" />
|
||||
<span>{g.name}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
interface ProblemHostsProps {
|
||||
hosts: ZBXHost[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class ProblemHosts extends PureComponent<ProblemHostsProps> {
|
||||
render() {
|
||||
return this.props.hosts.map((h) => (
|
||||
<div className={this.props.className || ''} key={h.hostid}>
|
||||
<FAIcon icon="server" />
|
||||
<span>{h.name}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
}
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
problemDetails: css`
|
||||
position: relative;
|
||||
flex: 10 1 auto;
|
||||
// padding: 0.5rem 1rem 0.5rem 1.2rem;
|
||||
padding: ${theme.spacing(0.5)} ${theme.spacing(1)} ${theme.spacing(0.5)} ${theme.spacing(1.2)}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// white-space: pre-line;
|
||||
`,
|
||||
});
|
||||
|
||||
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
|
||||
import moment from 'moment';
|
||||
import { isNewProblem } from '../../utils';
|
||||
import EventTag from '../EventTag';
|
||||
import { EventTag } from '../EventTag';
|
||||
import { ProblemDetails } from './ProblemDetails';
|
||||
import { AckProblemData } from '../AckModal';
|
||||
import { FAIcon, GFHeartIcon } from '../../../components';
|
||||
@@ -173,7 +173,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
Cell: statusIconCell,
|
||||
},
|
||||
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
|
||||
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell },
|
||||
{ Header: 'Problem', accessor: 'name', minWidth: 200, Cell: ProblemCell },
|
||||
{
|
||||
Header: 'Ack',
|
||||
id: 'ack',
|
||||
|
||||
@@ -102,6 +102,7 @@ export const plugin = new PanelPlugin<ProblemsPanelOptions, {}>(ProblemsPanel)
|
||||
path: 'triggerSeverity',
|
||||
name: 'Problem colors',
|
||||
editor: ProblemColorEditor,
|
||||
defaultValue: defaultPanelOptions.triggerSeverity,
|
||||
category: ['Colors'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface ProblemsPanelOptions {
|
||||
markAckEvents?: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SEVERITY = [
|
||||
export const DEFAULT_SEVERITY: TriggerSeverity[] = [
|
||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true },
|
||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true },
|
||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true },
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"path": "img/screenshot-triggers.png"
|
||||
}
|
||||
],
|
||||
"version": "4.2.10",
|
||||
"version": "4.3.0-pre",
|
||||
"updated": "2022-09-01"
|
||||
},
|
||||
"includes": [
|
||||
|
||||
@@ -237,9 +237,9 @@
|
||||
transition-property: opacity, max-height;
|
||||
|
||||
&.show {
|
||||
max-height: 32rem;
|
||||
max-height: 40rem;
|
||||
opacity: 1;
|
||||
box-shadow: inset -3px 3px 10px $problem-container-shadow;
|
||||
box-shadow: inset -3px 3px 5px #33b5ec4f;
|
||||
}
|
||||
|
||||
.problem-details-row {
|
||||
@@ -268,6 +268,7 @@
|
||||
padding: 0.5rem 1rem 0.5rem 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.problem-description-row {
|
||||
@@ -281,7 +282,7 @@
|
||||
&:after {
|
||||
content: "";
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
position: inherit;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 70%;
|
||||
@@ -292,10 +293,19 @@
|
||||
|
||||
.description-label {
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
// font-style: italic;
|
||||
color: $text-muted;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.description-delimiter {
|
||||
border-bottom: solid 2px #f9f9f91c;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.description-expression {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.problem-age {
|
||||
@@ -306,11 +316,11 @@
|
||||
padding-top: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.problem-items-row {
|
||||
position: relative;
|
||||
height: 1.5rem;
|
||||
position: inherit;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
@@ -326,7 +336,7 @@
|
||||
}
|
||||
|
||||
.problem-item {
|
||||
display: flex;
|
||||
display: inherit;
|
||||
|
||||
.problem-item-name {
|
||||
color: $text-muted;
|
||||
@@ -334,6 +344,9 @@
|
||||
|
||||
.problem-item-value {
|
||||
font-weight: 500;
|
||||
overflow: auto;
|
||||
display: -webkit-box;
|
||||
max-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +425,7 @@
|
||||
.problem-ack-list {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
white-space: pre-line;
|
||||
|
||||
.problem-ack-col {
|
||||
display: flex;
|
||||
|
||||
@@ -9,7 +9,7 @@ $zbx-card-background-stop: rgba(38, 38, 40, 0.8);
|
||||
$action-button-color: $blue-dark;
|
||||
$action-button-text-color: $gray-4;
|
||||
|
||||
$problems-border-color: #353535;
|
||||
$problems-border-color: #33b5e554;
|
||||
$problems-table-stripe: $dark-3;
|
||||
$problems-table-row-hovered: lighten($problems-table-stripe, 4%);
|
||||
$problems-table-row-hovered-shadow-color: rgba($blue, 0.5);
|
||||
|
||||
Reference in New Issue
Block a user