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,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,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