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:
@@ -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,30 +246,87 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
onChange={onFilterChange('host')}
|
||||
/>
|
||||
</InlineField>
|
||||
{query.countTriggersBy === 'problems' && (
|
||||
<InlineField label="Proxy" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.proxy?.filter}
|
||||
options={proxiesOptions}
|
||||
isLoading={proxiesLoading}
|
||||
onChange={onFilterChange('proxy')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow>
|
||||
{(supportsApplications || query.countTriggersBy !== 'items') && (
|
||||
<InlineField label="Application" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.application?.filter}
|
||||
options={appOptions}
|
||||
isLoading={appsLoading}
|
||||
onChange={onFilterChange('application')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
{!supportsApplications && query.countTriggersBy === 'items' && (
|
||||
<InlineField label="Item tag" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.itemTag.filter}
|
||||
options={tagOptions}
|
||||
isLoading={tagsLoading}
|
||||
onChange={onFilterChange('itemTag')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
{query.countTriggersBy === 'problems' && (
|
||||
<>
|
||||
<InlineField label="Problem" labelWidth={12}>
|
||||
<Input
|
||||
width={24}
|
||||
defaultValue={query.trigger?.filter}
|
||||
placeholder="Problem name"
|
||||
onBlur={onTextFilterChange('trigger')}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Tags" labelWidth={12}>
|
||||
<Input
|
||||
width={24}
|
||||
defaultValue={query.tags?.filter}
|
||||
placeholder="tag1:value1, tag2:value2"
|
||||
onBlur={onTextFilterChange('tags')}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
{query.countTriggersBy === 'items' && (
|
||||
<InlineField label="Item" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.item.filter}
|
||||
options={itemOptions}
|
||||
isLoading={itemsLoading}
|
||||
onChange={onFilterChange('item')}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow>
|
||||
<InlineField label="Application" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
value={query.application?.filter}
|
||||
options={appOptions}
|
||||
isLoading={appsLoading}
|
||||
onChange={onFilterChange('application')}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Min severity" labelWidth={12}>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
width={24}
|
||||
value={query.triggers?.minSeverity}
|
||||
value={query.options?.minSeverity}
|
||||
options={severityOptions}
|
||||
onChange={onMinSeverityChange}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Count" labelWidth={12}>
|
||||
<InlineSwitch
|
||||
value={query.triggers?.count}
|
||||
onChange={() => onChange({ ...query, triggers: { ...query.triggers, count: !query.triggers?.count } })}
|
||||
value={query.options?.count}
|
||||
onChange={() => onChange({ ...query, options: { ...query.options, count: !query.options?.count } })}
|
||||
/>
|
||||
</InlineField>
|
||||
</QueryEditorRow>
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user