Migrate from DatasourceAPI to DatasourceWithBackend (#2123)

This PR migrates the use of `DatasourceApi` to `DatasourceWithBackend`,
with this a couple additional improvements were made:

1. Migrate to use `interpolateVariablesInQuery` everywhere instead of
the custom `replaceTemplateVariables` we were using
2. Moves util functions out of `datasource.ts` and into the existing
`utils.ts`

<img width="1261" height="406" alt="Screenshot 2025-11-20 at 11 37
56 AM"
src="https://github.com/user-attachments/assets/9e396cf2-eab0-49d1-958c-963a2e896eba"
/>

Now we can see the `query` calls being made to the backend:
<img width="367" height="102" alt="Screenshot 2025-11-20 at 11 38 18 AM"
src="https://github.com/user-attachments/assets/a5a9a337-7f19-4f7c-9d04-9d30c0216fb2"
/>

Tested:
- By running queries from Explore and Dashboards (with and without
variables)
- By interacting with all the different Editors to make sure `ComboBox`
was working as expected


Next:
Once this is merged, we will next be able to slowly move away from using
the `ZabbixConnector` to make backend datasource calls.

Fixes:
[#131](https://github.com/orgs/grafana/projects/457/views/40?pane=issue&itemId=139450234&issue=grafana%7Coss-big-tent-squad%7C131)
This commit is contained in:
Jocelyn Collado-Kuri
2025-12-16 09:58:02 -08:00
committed by GitHub
parent cc492b916d
commit ce4a8d3e19
13 changed files with 658 additions and 564 deletions

View File

@@ -0,0 +1,5 @@
---
'grafana-zabbix': major
---
Migrates use of DatasourceApi to DatasourceWithBackend

View File

@@ -9,6 +9,7 @@ import { QueryEditorRow } from './QueryEditor/QueryEditorRow';
import { MetricPicker } from '../../components'; import { MetricPicker } from '../../components';
import { getVariableOptions } from './QueryEditor/utils'; import { getVariableOptions } from './QueryEditor/utils';
import { prepareAnnotation } from '../migrations'; import { prepareAnnotation } from '../migrations';
import { useInterpolatedQuery } from '../hooks/useInterpolatedQuery';
const severityOptions: Array<SelectableValue<number>> = [ const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' }, { value: 0, label: 'Not classified' },
@@ -27,6 +28,7 @@ type Props = ZabbixQueryEditorProps & {
export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasource }: Props) => { export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasource }: Props) => {
annotation = prepareAnnotation(annotation); annotation = prepareAnnotation(annotation);
const query = annotation.target; const query = annotation.target;
const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups(); const groups = await datasource.zabbix.getAllGroups();
@@ -44,8 +46,7 @@ export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasour
}, []); }, []);
const loadHostOptions = async (group: string) => { const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const hosts = await datasource.zabbix.getAllHosts(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({ let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name, value: host.name,
label: host.name, label: host.name,
@@ -57,14 +58,12 @@ export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasour
}; };
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter); const options = await loadHostOptions(interpolatedQuery.group.filter);
return options; return options;
}, [query.group.filter]); }, [interpolatedQuery.group.filter]);
const loadAppOptions = async (group: string, host: string) => { const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const apps = await datasource.zabbix.getAllApps(group, host);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({ let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name, value: app.name,
label: app.name, label: app.name,
@@ -75,13 +74,13 @@ export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasour
}; };
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => { const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter); const options = await loadAppOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
// Update suggestions on every metric change // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = interpolatedQuery.group?.filter;
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = interpolatedQuery.host?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();

View File

@@ -7,7 +7,22 @@ jest.mock('@grafana/runtime', () => ({
config: {}, config: {},
})); }));
jest.mock('@grafana/ui', () => ({
...jest.requireActual('@grafana/ui'),
config: {},
}));
describe('ConfigEditor', () => { describe('ConfigEditor', () => {
beforeAll(() => {
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
value: () => ({
measureText: () => ({ width: 0 }),
font: '',
textAlign: '',
}),
});
});
describe('on initial render', () => { describe('on initial render', () => {
it('should not mutate the options object', () => { it('should not mutate the options object', () => {
const options = Object.freeze({ ...getDefaultOptions() }); // freezing the options to prevent mutations const options = Object.freeze({ ...getDefaultOptions() }); // freezing the options to prevent mutations
@@ -27,7 +42,7 @@ describe('ConfigEditor', () => {
const onOptionsChangeSpy = jest.fn(); const onOptionsChangeSpy = jest.fn();
expect(() => render(<ConfigEditor options={options} onOptionsChange={onOptionsChangeSpy} />)).not.toThrow(); expect(() => render(<ConfigEditor options={options} onOptionsChange={onOptionsChangeSpy} />)).not.toThrow();
expect(onOptionsChangeSpy).toBeCalledTimes(1); expect(onOptionsChangeSpy).toHaveBeenCalledTimes(1);
expect(onOptionsChangeSpy).toHaveBeenCalledWith({ expect(onOptionsChangeSpy).toHaveBeenCalledWith({
...getDefaultOptions(), ...getDefaultOptions(),
jsonData: { jsonData: {
@@ -51,7 +66,7 @@ describe('ConfigEditor', () => {
const onOptionsChangeSpy = jest.fn(); const onOptionsChangeSpy = jest.fn();
expect(() => render(<ConfigEditor options={options} onOptionsChange={onOptionsChangeSpy} />)).not.toThrow(); expect(() => render(<ConfigEditor options={options} onOptionsChange={onOptionsChangeSpy} />)).not.toThrow();
expect(onOptionsChangeSpy).toBeCalledTimes(1); expect(onOptionsChangeSpy).toHaveBeenCalledTimes(1);
expect(onOptionsChangeSpy).toHaveBeenCalledWith({ expect(onOptionsChangeSpy).toHaveBeenCalledWith({
...getDefaultOptions(), ...getDefaultOptions(),
jsonData: { jsonData: {

View File

@@ -11,6 +11,7 @@ import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types/query'; import { ZabbixMetricsQuery } from '../../types/query';
import { ZBXItem, ZBXItemTag } from '../../types'; import { ZBXItem, ZBXItemTag } from '../../types';
import { itemTagToString } from '../../utils'; import { itemTagToString } from '../../utils';
import { useInterpolatedQuery } from '../../hooks/useInterpolatedQuery';
export interface Props { export interface Props {
query: ZabbixMetricsQuery; query: ZabbixMetricsQuery;
@@ -19,6 +20,8 @@ export interface Props {
} }
export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => { export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups(); const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({ const options = groups?.map((group) => ({
@@ -35,8 +38,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
}, []); }, []);
const loadHostOptions = async (group: string) => { const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const hosts = await datasource.zabbix.getAllHosts(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({ let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name, value: host.name,
label: host.name, label: host.name,
@@ -48,14 +50,12 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter); const options = await loadHostOptions(interpolatedQuery.group.filter);
return options; return options;
}, [query.group.filter]); }, [interpolatedQuery.group.filter]);
const loadAppOptions = async (group: string, host: string) => { const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const apps = await datasource.zabbix.getAllApps(group, host);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({ let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name, value: app.name,
label: app.name, label: app.name,
@@ -66,9 +66,9 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => { const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter); const options = await loadAppOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
const loadTagOptions = async (group: string, host: string) => { const loadTagOptions = async (group: string, host: string) => {
const tagsAvailable = await datasource.zabbix.isZabbix54OrHigher(); const tagsAvailable = await datasource.zabbix.isZabbix54OrHigher();
@@ -76,9 +76,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
return []; return [];
} }
const groupFilter = datasource.replaceTemplateVars(group); const items = await datasource.zabbix.getAllItems(group, host, null, null, {});
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 tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => item.tags || []));
// const tags: ZBXItemTag[] = await datasource.zabbix.getItemTags(groupFilter, hostFilter, null); // const tags: ZBXItemTag[] = await datasource.zabbix.getItemTags(groupFilter, hostFilter, null);
@@ -93,20 +91,16 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: tagsLoading, value: tagOptions }, fetchTags] = useAsyncFn(async () => { const [{ loading: tagsLoading, value: tagOptions }, fetchTags] = useAsyncFn(async () => {
const options = await loadTagOptions(query.group.filter, query.host.filter); const options = await loadTagOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => { 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 = { const options = {
itemtype: 'num', itemtype: 'num',
showDisabledItems: query.options.showDisabledItems, showDisabledItems: query.options.showDisabledItems,
}; };
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options); const items = await datasource.zabbix.getAllItems(group, host, app, itemTag, options);
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({ let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name, value: item.name,
label: item.name, label: item.name,
@@ -118,19 +112,24 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => { const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions( const options = await loadItemOptions(
query.group.filter, interpolatedQuery.group.filter,
query.host.filter, interpolatedQuery.host.filter,
query.application.filter, interpolatedQuery.application.filter,
query.itemTag.filter interpolatedQuery.itemTag.filter
); );
return options; return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]); }, [
interpolatedQuery.group.filter,
interpolatedQuery.host.filter,
interpolatedQuery.application.filter,
interpolatedQuery.itemTag.filter,
]);
// Update suggestions on every metric change // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = interpolatedQuery.group?.filter;
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = interpolatedQuery.host?.filter;
const appFilter = datasource.replaceTemplateVars(query.application?.filter); const appFilter = interpolatedQuery.application?.filter;
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter); const tagFilter = interpolatedQuery.itemTag?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();

View File

@@ -9,6 +9,7 @@ import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils'; import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource'; import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery, ZabbixTagEvalType } from '../../types/query'; import { ZabbixMetricsQuery, ZabbixTagEvalType } from '../../types/query';
import { useInterpolatedQuery } from '../../hooks/useInterpolatedQuery';
const showProblemsOptions: Array<SelectableValue<string>> = [ const showProblemsOptions: Array<SelectableValue<string>> = [
{ label: 'Problems', value: 'problems' }, { label: 'Problems', value: 'problems' },
@@ -37,6 +38,8 @@ export interface Props {
} }
export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => { export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups(); const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({ const options = groups?.map((group) => ({
@@ -53,8 +56,7 @@ export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
}, []); }, []);
const loadHostOptions = async (group: string) => { const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const hosts = await datasource.zabbix.getAllHosts(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({ let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name, value: host.name,
label: host.name, label: host.name,
@@ -66,14 +68,12 @@ export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter); const options = await loadHostOptions(interpolatedQuery.group.filter);
return options; return options;
}, [query.group.filter]); }, [interpolatedQuery.group.filter]);
const loadAppOptions = async (group: string, host: string) => { const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const apps = await datasource.zabbix.getAllApps(group, host);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({ let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name, value: app.name,
label: app.name, label: app.name,
@@ -84,9 +84,9 @@ export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => { const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter); const options = await loadAppOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
const loadProxyOptions = async () => { const loadProxyOptions = async () => {
const proxies = await datasource.zabbix.getProxies(); const proxies = await datasource.zabbix.getProxies();
@@ -104,8 +104,8 @@ export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
}, []); }, []);
// Update suggestions on every metric change // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = interpolatedQuery.group?.filter;
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = interpolatedQuery.host?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();

View File

@@ -9,6 +9,7 @@ import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils'; import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource'; import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types/query'; import { ZabbixMetricsQuery } from '../../types/query';
import { useInterpolatedQuery } from '../../hooks/useInterpolatedQuery';
export interface Props { export interface Props {
query: ZabbixMetricsQuery; query: ZabbixMetricsQuery;
@@ -17,6 +18,8 @@ export interface Props {
} }
export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) => { export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups(); const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({ const options = groups?.map((group) => ({
@@ -33,8 +36,7 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
}, []); }, []);
const loadHostOptions = async (group: string) => { const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const hosts = await datasource.zabbix.getAllHosts(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({ let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name, value: host.name,
label: host.name, label: host.name,
@@ -46,14 +48,12 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
}; };
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter); const options = await loadHostOptions(interpolatedQuery.group.filter);
return options; return options;
}, [query.group.filter]); }, [interpolatedQuery.group.filter]);
const loadAppOptions = async (group: string, host: string) => { const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const apps = await datasource.zabbix.getAllApps(group, host);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({ let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name, value: app.name,
label: app.name, label: app.name,
@@ -64,20 +64,16 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
}; };
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => { const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter); const options = await loadAppOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => { 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 = { const options = {
itemtype: 'text', itemtype: 'text',
showDisabledItems: query.options.showDisabledItems, showDisabledItems: query.options.showDisabledItems,
}; };
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options); const items = await datasource.zabbix.getAllItems(group, host, app, itemTag, options);
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({ let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name, value: item.name,
label: item.name, label: item.name,
@@ -89,19 +85,24 @@ export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) =
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => { const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions( const options = await loadItemOptions(
query.group.filter, interpolatedQuery.group.filter,
query.host.filter, interpolatedQuery.host.filter,
query.application.filter, interpolatedQuery.application.filter,
query.itemTag.filter interpolatedQuery.itemTag.filter
); );
return options; return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]); }, [
interpolatedQuery.group.filter,
interpolatedQuery.host.filter,
interpolatedQuery.application.filter,
interpolatedQuery.itemTag.filter,
]);
// Update suggestions on every metric change // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = interpolatedQuery.group?.filter;
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = interpolatedQuery.host?.filter;
const appFilter = datasource.replaceTemplateVars(query.application?.filter); const appFilter = interpolatedQuery.application?.filter;
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter); const tagFilter = interpolatedQuery.itemTag?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();

View File

@@ -11,6 +11,7 @@ import { itemTagToString } from '../../utils';
import { ZabbixDatasource } from '../../datasource'; import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types/query'; import { ZabbixMetricsQuery } from '../../types/query';
import { ZBXItem, ZBXItemTag } from '../../types'; import { ZBXItem, ZBXItemTag } from '../../types';
import { useInterpolatedQuery } from '../../hooks/useInterpolatedQuery';
const countByOptions: Array<SelectableValue<string>> = [ const countByOptions: Array<SelectableValue<string>> = [
{ value: '', label: 'All triggers' }, { value: '', label: 'All triggers' },
@@ -34,6 +35,8 @@ export interface Props {
} }
export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => { export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups(); const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({ const options = groups?.map((group) => ({
@@ -50,8 +53,7 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
}, []); }, []);
const loadHostOptions = async (group: string) => { const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const hosts = await datasource.zabbix.getAllHosts(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({ let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name, value: host.name,
label: host.name, label: host.name,
@@ -63,14 +65,12 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter); const options = await loadHostOptions(interpolatedQuery.group.filter);
return options; return options;
}, [query.group.filter]); }, [interpolatedQuery.group.filter]);
const loadAppOptions = async (group: string, host: string) => { const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const apps = await datasource.zabbix.getAllApps(group, host);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({ let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name, value: app.name,
label: app.name, label: app.name,
@@ -81,19 +81,16 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => { const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter); const options = await loadAppOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
const loadTagOptions = async (group: string, host: string) => { const loadTagOptions = async (group: string, host: string) => {
const tagsAvailable = await datasource.zabbix.isZabbix54OrHigher(); const tagsAvailable = await datasource.zabbix.isZabbix54OrHigher();
if (!tagsAvailable) { if (!tagsAvailable) {
return []; return [];
} }
const items = await datasource.zabbix.getAllItems(group, host, null, null, {});
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 tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => item.tags || []));
const tagList = _.uniqBy(tags, (t) => t.tag + t.value || '').map((t) => itemTagToString(t)); const tagList = _.uniqBy(tags, (t) => t.tag + t.value || '').map((t) => itemTagToString(t));
@@ -107,9 +104,9 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
}; };
const [{ loading: tagsLoading, value: tagOptions }, fetchItemTags] = useAsyncFn(async () => { const [{ loading: tagsLoading, value: tagOptions }, fetchItemTags] = useAsyncFn(async () => {
const options = await loadTagOptions(query.group.filter, query.host.filter); const options = await loadTagOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
const loadProxyOptions = async () => { const loadProxyOptions = async () => {
const proxies = await datasource.zabbix.getProxies(); const proxies = await datasource.zabbix.getProxies();
@@ -127,15 +124,11 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
}, []); }, []);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => { 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 = { const options = {
itemtype: 'num', itemtype: 'num',
showDisabledItems: query.options.showDisabledItems, showDisabledItems: query.options.showDisabledItems,
}; };
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options); const items = await datasource.zabbix.getAllItems(group, host, app, itemTag, options);
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({ let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name, value: item.name,
label: item.name, label: item.name,
@@ -147,19 +140,24 @@ export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => { const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions( const options = await loadItemOptions(
query.group.filter, interpolatedQuery.group.filter,
query.host.filter, interpolatedQuery.host.filter,
query.application.filter, interpolatedQuery.application.filter,
query.itemTag.filter interpolatedQuery.itemTag.filter
); );
return options; return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]); }, [
interpolatedQuery.group.filter,
interpolatedQuery.host.filter,
interpolatedQuery.application.filter,
interpolatedQuery.itemTag.filter,
]);
// Update suggestions on every metric change // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = interpolatedQuery.group?.filter;
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = interpolatedQuery.host?.filter;
const appFilter = datasource.replaceTemplateVars(query.application?.filter); const appFilter = interpolatedQuery.application?.filter;
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter); const tagFilter = interpolatedQuery.itemTag?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();

View File

@@ -9,6 +9,7 @@ import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils'; import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource'; import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types/query'; import { ZabbixMetricsQuery } from '../../types/query';
import { useInterpolatedQuery } from '../../hooks/useInterpolatedQuery';
export interface Props { export interface Props {
query: ZabbixMetricsQuery; query: ZabbixMetricsQuery;
@@ -17,6 +18,7 @@ export interface Props {
} }
export const UserMacrosQueryEditor = ({ query, datasource, onChange }: Props) => { export const UserMacrosQueryEditor = ({ query, datasource, onChange }: Props) => {
const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups(); const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({ const options = groups?.map((group) => ({
@@ -33,8 +35,7 @@ export const UserMacrosQueryEditor = ({ query, datasource, onChange }: Props) =>
}, []); }, []);
const loadHostOptions = async (group: string) => { const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const hosts = await datasource.zabbix.getAllHosts(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({ let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name, value: host.name,
label: host.name, label: host.name,
@@ -46,14 +47,12 @@ export const UserMacrosQueryEditor = ({ query, datasource, onChange }: Props) =>
}; };
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter); const options = await loadHostOptions(interpolatedQuery.group.filter);
return options; return options;
}, [query.group.filter]); }, [interpolatedQuery.group.filter]);
const loadMacrosOptions = async (group: string, host: string) => { const loadMacrosOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group); const macros = await datasource.zabbix.getAllMacros(group, host);
const hostFilter = datasource.replaceTemplateVars(host);
const macros = await datasource.zabbix.getAllMacros(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = macros?.map((m) => ({ let options: Array<SelectableValue<string>> = macros?.map((m) => ({
value: m.macro, value: m.macro,
label: m.macro, label: m.macro,
@@ -65,13 +64,13 @@ export const UserMacrosQueryEditor = ({ query, datasource, onChange }: Props) =>
}; };
const [{ loading: macrosLoading, value: macrosOptions }, fetchmacros] = useAsyncFn(async () => { const [{ loading: macrosLoading, value: macrosOptions }, fetchmacros] = useAsyncFn(async () => {
const options = await loadMacrosOptions(query.group.filter, query.host.filter); const options = await loadMacrosOptions(interpolatedQuery.group.filter, interpolatedQuery.host.filter);
return options; return options;
}, [query.group.filter, query.host.filter]); }, [interpolatedQuery.group.filter, interpolatedQuery.host.filter]);
// Update suggestions on every metric change // Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter); const groupFilter = interpolatedQuery.group?.filter;
const hostFilter = datasource.replaceTemplateVars(query.host?.filter); const hostFilter = interpolatedQuery.host?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();

View File

@@ -6,7 +6,6 @@ import * as utils from './utils';
import * as migrations from './migrations'; import * as migrations from './migrations';
import * as metricFunctions from './metricFunctions'; import * as metricFunctions from './metricFunctions';
import * as c from './constants'; import * as c from './constants';
import dataProcessor from './dataProcessor';
import responseHandler from './responseHandler'; import responseHandler from './responseHandler';
import problemsHandler from './problemsHandler'; import problemsHandler from './problemsHandler';
import { Zabbix } from './zabbix/zabbix'; import { Zabbix } from './zabbix/zabbix';
@@ -18,28 +17,27 @@ import {
BackendSrvRequest, BackendSrvRequest,
getBackendSrv, getBackendSrv,
getTemplateSrv, getTemplateSrv,
toDataQueryResponse,
getDataSourceSrv, getDataSourceSrv,
HealthCheckError, HealthCheckError,
DataSourceWithBackend, DataSourceWithBackend,
TemplateSrv,
} from '@grafana/runtime'; } from '@grafana/runtime';
import { import {
DataFrame, DataFrame,
dataFrameFromJSON, dataFrameFromJSON,
DataQueryRequest, DataQueryRequest,
DataQueryResponse, DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
FieldType, FieldType,
isDataFrame, isDataFrame,
LoadingState, ScopedVars,
toDataFrame, toDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
import { trackRequest } from './tracking'; import { trackRequest } from './tracking';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom, map, Observable } from 'rxjs';
export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> { export class ZabbixDatasource extends DataSourceWithBackend<ZabbixMetricsQuery, ZabbixDSOptions> {
name: string; name: string;
basicAuth: any; basicAuth: any;
withCredentials: any; withCredentials: any;
@@ -59,9 +57,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>; instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>;
zabbix: Zabbix; zabbix: Zabbix;
replaceTemplateVars: (target: any, scopedVars?: any) => any; constructor(
instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>,
constructor(instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>) { private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings); super(instanceSettings);
this.instanceSettings = instanceSettings; this.instanceSettings = instanceSettings;
@@ -72,10 +71,6 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
prepareAnnotation: migrations.prepareAnnotation, prepareAnnotation: migrations.prepareAnnotation,
}; };
// Use custom format for template variables
const templateSrv = getTemplateSrv();
this.replaceTemplateVars = _.partial(replaceTemplateVars, templateSrv);
// General data source settings // General data source settings
this.datasourceId = instanceSettings.id; this.datasourceId = instanceSettings.id;
this.name = instanceSettings.name; this.name = instanceSettings.name;
@@ -120,13 +115,12 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
//////////////////////// ////////////////////////
// Datasource methods // // Datasource methods //
//////////////////////// ////////////////////////
/** /**
* Query panel data. Calls for each panel in dashboard. * Query panel data. Calls for each panel in dashboard.
* @param {Object} request Contains time range, targets and other info. * @param {Object} request Contains time range, targets and other info.
* @return {Object} Grafana metrics object with timeseries data for each target. * @return {Object} Grafana metrics object with timeseries data for each target.
*/ */
query(request: DataQueryRequest<ZabbixMetricsQuery>) { query(request: DataQueryRequest<ZabbixMetricsQuery>): Observable<DataQueryResponse> {
trackRequest(request); trackRequest(request);
// Migrate old targets // Migrate old targets
@@ -144,104 +138,33 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return target; return target;
}); });
const backendResponsePromise = this.backendQuery({ ...request, targets: requestTargets }); const interpolatedTargets = this.interpolateVariablesInQueries(requestTargets, request.scopedVars);
const dbConnectionResponsePromise = this.dbConnectionQuery({ ...request, targets: requestTargets }); const backendResponse = super.query({ ...request, targets: interpolatedTargets.filter(this.isBackendTarget) });
const frontendResponsePromise = this.frontendQuery({ ...request, targets: requestTargets }); const dbConnectionResponsePromise = this.dbConnectionQuery({ ...request, targets: interpolatedTargets });
const annotationResposePromise = this.annotationRequest({ ...request, targets: requestTargets }); const frontendResponsePromise = this.frontendQuery({ ...request, targets: interpolatedTargets });
const annotationResposePromise = this.annotationRequest({ ...request, targets: interpolatedTargets });
return Promise.all([ const applyMergeQueries = (queryResponse: DataQueryResponse) =>
backendResponsePromise, this.mergeQueries(queryResponse, dbConnectionResponsePromise, frontendResponsePromise, annotationResposePromise);
dbConnectionResponsePromise, const applyFEFuncs = (queryResponse: DataQueryResponse) =>
frontendResponsePromise, this.applyFrontendFunctions(queryResponse, {
annotationResposePromise, ...request,
]).then((rsp) => { targets: interpolatedTargets.filter(this.isBackendTarget),
// Merge backend and frontend queries results
const [backendRes, dbConnectionRes, frontendRes, annotationRes] = rsp;
if (dbConnectionRes.data) {
backendRes.data = backendRes.data.concat(dbConnectionRes.data);
}
if (frontendRes.data) {
backendRes.data = backendRes.data.concat(frontendRes.data);
}
if (annotationRes.data) {
backendRes.data = backendRes.data.concat(annotationRes.data);
}
return {
data: backendRes.data,
state: LoadingState.Done,
key: request.requestId,
};
}); });
}
async backendQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> { return backendResponse.pipe(
const { intervalMs, maxDataPoints, range, requestId } = request; map(applyFEFuncs),
const targets = request.targets.filter(this.isBackendTarget); map(responseHandler.convertZabbixUnits),
map(this.convertToWide),
// Add range variables map(applyMergeQueries)
request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
const queries = _.compact(
targets.map((query) => {
// Don't request for hidden targets
if (query.hide) {
return null;
}
this.replaceTargetVariables(query, request);
return {
...query,
datasourceId: this.datasourceId,
intervalMs,
maxDataPoints,
};
})
); );
// Return early if no queries exist
if (!queries.length) {
return Promise.resolve({ data: [] });
} }
const body: any = { queries }; async frontendQuery(request: DataQueryRequest<ZabbixMetricsQuery>): Promise<DataQueryResponse> {
if (range) {
body.range = range;
body.from = range.from.valueOf().toString();
body.to = range.to.valueOf().toString();
}
let rsp: any;
try {
rsp = await lastValueFrom(
getBackendSrv().fetch({
url: '/api/ds/query',
method: 'POST',
data: body,
requestId,
})
);
} catch (err) {
return toDataQueryResponse(err);
}
const resp = toDataQueryResponse(rsp);
this.sortByRefId(resp);
this.applyFrontendFunctions(resp, request);
responseHandler.convertZabbixUnits(resp);
if (responseHandler.isConvertibleToWide(resp.data)) {
console.log('Converting response to the wide format');
resp.data = responseHandler.convertToWide(resp.data);
}
return resp;
}
async frontendQuery(request: DataQueryRequest<any>): Promise<DataQueryResponse> {
const frontendTargets = request.targets.filter((t) => !(this.isBackendTarget(t) || this.isDBConnectionTarget(t))); const frontendTargets = request.targets.filter((t) => !(this.isBackendTarget(t) || this.isDBConnectionTarget(t)));
const oldVersionTargets = frontendTargets
.filter((target) => (target as any).itservice && !target.itServiceFilter)
.map((t) => t.refId);
const promises = _.map(frontendTargets, (target) => { const promises = _.map(frontendTargets, (target) => {
// Don't request for hidden targets // Don't request for hidden targets
if (target.hide) { if (target.hide) {
@@ -250,7 +173,6 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// Add range variables // Add range variables
request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range)); request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
this.replaceTargetVariables(target, request);
const timeRange = this.buildTimeRange(request, target); const timeRange = this.buildTimeRange(request, target);
if (target.queryType === c.MODE_TEXT) { if (target.queryType === c.MODE_TEXT) {
@@ -262,7 +184,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return this.queryTextData(target, timeRange); return this.queryTextData(target, timeRange);
} else if (target.queryType === c.MODE_ITSERVICE) { } else if (target.queryType === c.MODE_ITSERVICE) {
// IT services query // IT services query
return this.queryITServiceData(target, timeRange, request); const isOldVersion = oldVersionTargets.includes(target.refId);
return this.queryITServiceData(target, timeRange, request, isOldVersion);
} else if (target.queryType === c.MODE_TRIGGERS) { } else if (target.queryType === c.MODE_TRIGGERS) {
// Triggers query // Triggers query
return this.queryTriggersData(target, timeRange, request); return this.queryTriggersData(target, timeRange, request);
@@ -311,7 +234,6 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
// Add range variables // Add range variables
request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range)); request.scopedVars = Object.assign({}, request.scopedVars, utils.getRangeScopedVars(request.range));
this.replaceTargetVariables(target, request);
const timeRange = this.buildTimeRange(request, target); const timeRange = this.buildTimeRange(request, target);
const useTrends = this.isUseTrends(timeRange, target); const useTrends = this.isUseTrends(timeRange, target);
@@ -341,7 +263,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
let timeTo = Math.ceil(dateMath.parse(request.range.to) / 1000); let timeTo = Math.ceil(dateMath.parse(request.range.to) / 1000);
// Apply Time-related functions (timeShift(), etc) // Apply Time-related functions (timeShift(), etc)
const timeFunctions = bindFunctionDefs(target.functions, 'Time'); const timeFunctions = utils.bindFunctionDefs(target.functions, 'Time');
if (timeFunctions.length) { if (timeFunctions.length) {
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]); const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
timeFrom = time_from; timeFrom = time_from;
@@ -377,7 +299,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
async queryNumericDataForItems(items, target: ZabbixMetricsQuery, timeRange, useTrends, request) { async queryNumericDataForItems(items, target: ZabbixMetricsQuery, timeRange, useTrends, request) {
let history; let history;
request.valueType = this.getTrendValueType(target); request.valueType = this.getTrendValueType(target);
request.consolidateBy = getConsolidateBy(target) || request.valueType; request.consolidateBy = utils.getConsolidateBy(target) || request.valueType;
if (useTrends) { if (useTrends) {
history = await this.zabbix.getTrends(items, timeRange, request); history = await this.zabbix.getTrends(items, timeRange, request);
@@ -454,10 +376,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
applyFrontendFunctions(response: DataQueryResponse, request: DataQueryRequest<any>) { applyFrontendFunctions(response: DataQueryResponse, request: DataQueryRequest<any>) {
for (let i = 0; i < response.data.length; i++) { for (let i = 0; i < response.data.length; i++) {
const frame: DataFrame = response.data[i]; const frame: DataFrame = response.data[i];
const target = getRequestTarget(request, frame.refId); const target = utils.getRequestTarget(request, frame.refId);
// Apply alias functions // Apply alias functions
const aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); const aliasFunctions = utils.bindFunctionDefs(target.functions, 'Alias');
utils.sequence(aliasFunctions)(frame); utils.sequence(aliasFunctions)(frame);
} }
return response; return response;
@@ -489,7 +411,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
queryItemIdData(target, timeRange, useTrends, options) { queryItemIdData(target, timeRange, useTrends, options) {
let itemids = target.itemids; let itemids = target.itemids;
const templateSrv = getTemplateSrv(); const templateSrv = getTemplateSrv();
itemids = templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat); itemids = templateSrv.replace(itemids, options.scopedVars, utils.zabbixItemIdsTemplateFormat);
itemids = _.map(itemids.split(','), (itemid) => itemid.trim()); itemids = _.map(itemids.split(','), (itemid) => itemid.trim());
if (!itemids) { if (!itemids) {
@@ -504,34 +426,26 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
/** /**
* Query target data for IT Services * Query target data for IT Services
*/ */
async queryITServiceData(target: ZabbixMetricsQuery, timeRange, request) { async queryITServiceData(
target: ZabbixMetricsQuery,
timeRange: number[],
request: DataQueryRequest<ZabbixMetricsQuery>,
isOldVersion: boolean
) {
// Don't show undefined and hidden targets // Don't show undefined and hidden targets
if (target.hide || (!(target as any).itservice && !target.itServiceFilter) || !target.slaProperty) { if (target.hide || (!(target as any).itservice && !target.itServiceFilter) || !target.slaProperty) {
return []; return [];
} }
let itServiceFilter; let itservices = await this.zabbix.getITServices(target.itServiceFilter);
request.isOldVersion = (target as any).itservice && !target.itServiceFilter; if (isOldVersion) {
if (request.isOldVersion) {
// Backward compatibility
itServiceFilter = '/.*/';
} else {
itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, request.scopedVars);
}
request.slaInterval = target.slaInterval;
let itservices = await this.zabbix.getITServices(itServiceFilter);
if (request.isOldVersion) {
itservices = _.filter(itservices, { serviceid: (target as any).itservice?.serviceid }); itservices = _.filter(itservices, { serviceid: (target as any).itservice?.serviceid });
} }
if (target.slaFilter !== undefined) { if (target.slaFilter !== undefined) {
const slaFilter = this.replaceTemplateVars(target.slaFilter, request.scopedVars); const slas = await this.zabbix.getSLAs(target.slaFilter);
const slas = await this.zabbix.getSLAs(slaFilter);
const result = await this.zabbix.getSLI(itservices, slas, timeRange, target, request); const result = await this.zabbix.getSLI(itservices, slas, timeRange, target, request);
// Apply alias functions // Apply alias functions
const aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); const aliasFunctions = utils.bindFunctionDefs(target.functions, 'Alias');
utils.sequence(aliasFunctions)(result); utils.sequence(aliasFunctions)(result);
return result; return result;
} }
@@ -568,11 +482,10 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
const hostids = hosts?.map((h) => h.hostid); const hostids = hosts?.map((h) => h.hostid);
const appids = apps?.map((a) => a.applicationid); const appids = apps?.map((a) => a.applicationid);
const options = getTriggersOptions(target, timeRange); const options = utils.getTriggersOptions(target, timeRange);
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, request.scopedVars); // variable interpolation builds regex-like string, so we should trim it.
// replaceTemplateVars() builds regex-like string, so we should trim it. const tagsFilterStr = target.tags.filter.replace('/^', '').replace('$/', '');
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', '');
const tags = utils.parseTags(tagsFilterStr); const tags = utils.parseTags(tagsFilterStr);
tags.forEach((tag) => { tags.forEach((tag) => {
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal // Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
@@ -603,16 +516,15 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
return Promise.resolve([]); return Promise.resolve([]);
} }
const options = getTriggersOptions(target, timeRange); const options = utils.getTriggersOptions(target, timeRange);
const alerts = await this.zabbix.getHostICAlerts(hostids, appids, itemids, options); const alerts = await this.zabbix.getHostICAlerts(hostids, appids, itemids, options);
return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target); return responseHandler.handleTriggersResponse(alerts, groups, timeRange, target);
} }
async queryTriggersPCData(target: ZabbixMetricsQuery, timeRange, request) { async queryTriggersPCData(target: ZabbixMetricsQuery, timeRange, request) {
const [timeFrom, timeTo] = timeRange; const [timeFrom, timeTo] = timeRange;
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, request.scopedVars); // variable interpolation builds regex-like string, so we should trim it.
// replaceTemplateVars() builds regex-like string, so we should trim it. const tagsFilterStr = target.tags.filter.replace('/^', '').replace('$/', '');
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', '');
const tags = utils.parseTags(tagsFilterStr); const tags = utils.parseTags(tagsFilterStr);
tags.forEach((tag) => { tags.forEach((tag) => {
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal // Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
@@ -682,23 +594,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
const getProxiesPromise = showProxy ? this.zabbix.getProxies() : () => []; const getProxiesPromise = showProxy ? this.zabbix.getProxies() : () => [];
showAckButton = !this.disableReadOnlyUsersAck || userIsEditor; showAckButton = !this.disableReadOnlyUsersAck || userIsEditor;
// Replace template variables
const groupFilter = this.replaceTemplateVars(target.group?.filter, options.scopedVars);
const hostFilter = this.replaceTemplateVars(target.host?.filter, options.scopedVars);
const appFilter = this.replaceTemplateVars(target.application?.filter, options.scopedVars);
const proxyFilter = this.replaceTemplateVars(target.proxy?.filter, options.scopedVars);
const triggerFilter = this.replaceTemplateVars(target.trigger?.filter, options.scopedVars);
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, options.scopedVars);
const replacedTarget = {
...target,
trigger: { filter: triggerFilter },
tags: { filter: tagsFilter },
};
// replaceTemplateVars() builds regex-like string, so we should trim it. // replaceTemplateVars() builds regex-like string, so we should trim it.
const tagsFilterStr = tagsFilter.replace('/^', '').replace('$/', ''); const tagsFilterStr = target.tags.filter.replace('/^', '').replace('$/', '');
const tags = utils.parseTags(tagsFilterStr); const tags = utils.parseTags(tagsFilterStr);
tags.forEach((tag) => { tags.forEach((tag) => {
// Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal // Zabbix uses {"tag": "<tag>", "value": "<value>", "operator": "<operator>"} format, where 1 means Equal
@@ -733,14 +630,20 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
problemsOptions.timeFrom = timeFrom; problemsOptions.timeFrom = timeFrom;
problemsOptions.timeTo = timeTo; problemsOptions.timeTo = timeTo;
getProblemsPromise = this.zabbix.getProblemsHistory( getProblemsPromise = this.zabbix.getProblemsHistory(
groupFilter, target.group.filter,
hostFilter, target.host.filter,
appFilter, target.application.filter,
proxyFilter, target.proxy.filter,
problemsOptions problemsOptions
); );
} else { } else {
getProblemsPromise = this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions); getProblemsPromise = this.zabbix.getProblems(
target.group.filter,
target.host.filter,
target.application.filter,
target.proxy.filter,
problemsOptions
);
} }
const getUsersPromise = this.zabbix.getUsers(); const getUsersPromise = this.zabbix.getUsers();
@@ -754,7 +657,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
}) })
.then((problems) => problemsHandler.setMaintenanceStatus(problems)) .then((problems) => problemsHandler.setMaintenanceStatus(problems))
.then((problems) => problemsHandler.setAckButtonStatus(problems, showAckButton)) .then((problems) => problemsHandler.setAckButtonStatus(problems, showAckButton))
.then((problems) => problemsHandler.filterTriggersPre(problems, replacedTarget)) .then((problems) => problemsHandler.filterTriggersPre(problems, target))
.then((problems) => problemsHandler.sortProblems(problems, target)) .then((problems) => problemsHandler.sortProblems(problems, target))
.then((problems) => problemsHandler.addTriggerDataSource(problems, target)) .then((problems) => problemsHandler.addTriggerDataSource(problems, target))
.then((problems) => problemsHandler.formatAcknowledges(problems, zabbixUsers)) .then((problems) => problemsHandler.formatAcknowledges(problems, zabbixUsers))
@@ -770,9 +673,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
* Test connection to Zabbix API and external history DB. * Test connection to Zabbix API and external history DB.
*/ */
async testDatasource() { async testDatasource() {
const backendDS = new DataSourceWithBackend(this.instanceSettings);
try { try {
const testResult = await backendDS.testDatasource(); const testResult = await super.testDatasource();
return this.zabbix.testDataSource().then((dbConnectorStatus) => { return this.zabbix.testDataSource().then((dbConnectorStatus) => {
let message = testResult.message; let message = testResult.message;
if (dbConnectorStatus) { if (dbConnectorStatus) {
@@ -839,7 +741,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
} }
for (const prop of ['group', 'host', 'application', 'itemTag', 'item']) { for (const prop of ['group', 'host', 'application', 'itemTag', 'item']) {
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {}); queryModel[prop] = utils.replaceTemplateVars(this.templateSrv, queryModel[prop], {});
} }
queryModel = queryModel as VariableQuery; queryModel = queryModel as VariableQuery;
@@ -878,7 +780,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
} }
return resultPromise.then((metrics) => { return resultPromise.then((metrics) => {
return _.map(metrics, formatMetric); return _.map(metrics, utils.formatMetric);
}); });
} }
@@ -931,16 +833,16 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
problemsOptions.severities = severities; problemsOptions.severities = severities;
} }
const groupFilter = this.replaceTemplateVars(annotation.group.filter, {}); const groupFilter = annotation.group.filter;
const hostFilter = this.replaceTemplateVars(annotation.host.filter, {}); const hostFilter = annotation.host.filter;
const appFilter = this.replaceTemplateVars(annotation.application.filter, {}); const appFilter = annotation.application.filter;
const proxyFilter = undefined; const proxyFilter = undefined;
return this.zabbix return this.zabbix
.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions) .getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
.then((problems) => { .then((problems) => {
// Filter triggers by description // Filter triggers by description
const problemName = this.replaceTemplateVars(annotation.trigger.filter, {}); const problemName = annotation.trigger.filter;
if (utils.isRegex(problemName)) { if (utils.isRegex(problemName)) {
problems = _.filter(problems, (p) => { problems = _.filter(problems, (p) => {
return utils.buildRegex(problemName).test(p.description); return utils.buildRegex(problemName).test(p.description);
@@ -977,35 +879,6 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
}); });
} }
// Replace template variables
replaceTargetVariables(target, options) {
const templateSrv = getTemplateSrv();
const parts = ['group', 'host', 'application', 'itemTag', 'item'];
_.forEach(parts, (p) => {
if (target[p] && target[p].filter) {
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
}
});
if (target.textFilter) {
target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars);
}
if (target.itemids) {
target.itemids = templateSrv.replace(target.itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
}
_.forEach(target.functions, (func) => {
func.params = _.map(func.params, (param) => {
if (typeof param === 'number') {
return +templateSrv.replace(param.toString(), options.scopedVars);
} else {
return templateSrv.replace(param, options.scopedVars);
}
});
});
}
isUseTrends(timeRange, target: ZabbixMetricsQuery) { isUseTrends(timeRange, target: ZabbixMetricsQuery) {
if (target.options.useTrends === 'false') { if (target.options.useTrends === 'false') {
return false; return false;
@@ -1030,108 +903,89 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
isDBConnectionTarget = (target: any): boolean => { isDBConnectionTarget = (target: any): boolean => {
return this.enableDirectDBConnection && (target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID); return this.enableDirectDBConnection && (target.queryType === c.MODE_METRICS || target.queryType === c.MODE_ITEMID);
}; };
mergeQueries(
queryResponse: DataQueryResponse,
dbConnectionResponsePromise: Promise<DataQueryResponse>,
frontendResponsePromise: Promise<DataQueryResponse>,
annotationResposePromise: Promise<DataQueryResponse>
): DataQueryResponse {
Promise.all([dbConnectionResponsePromise, frontendResponsePromise, annotationResposePromise]).then((resp) => {
const [dbConnectionRes, frontendRes, annotationRes] = resp;
if (dbConnectionRes.data) {
queryResponse.data = queryResponse.data.concat(dbConnectionRes.data);
}
if (frontendRes.data) {
queryResponse.data = queryResponse.data.concat(frontendRes.data);
} }
function bindFunctionDefs(functionDefs, category) { if (annotationRes.data) {
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); queryResponse.data = queryResponse.data.concat(annotationRes.data);
const aggFuncDefs = _.filter(functionDefs, (func) => { }
return _.includes(aggregationFunctions, func.def.name) && func.params.length > 0;
});
return _.map(aggFuncDefs, (func) => {
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(dataProcessor.metricFunctions);
}); });
return queryResponse;
} }
function getConsolidateBy(target) { convertToWide(response: DataQueryResponse) {
let consolidateBy; if (responseHandler.isConvertibleToWide(response.data)) {
const funcDef = _.find(target.functions, (func) => { response.data = responseHandler.convertToWide(response.data);
return func.def.name === 'consolidateBy';
});
if (funcDef && funcDef.params && funcDef.params.length) {
consolidateBy = funcDef.params[0];
} }
return consolidateBy; return response;
} }
interpolateVariablesInQueries(queries: ZabbixMetricsQuery[], scopedVars: ScopedVars): ZabbixMetricsQuery[] {
function formatMetric(metricObj) { if (!queries || queries.length === 0) {
return [];
}
return queries.map((query) => {
// backwardsCompatibility
const isOldVersion: boolean = (query as any).itservice && !query.itServiceFilter;
return { return {
text: metricObj.name, ...query,
expandable: false, itServiceFilter: isOldVersion
? '/.*/'
: utils.replaceTemplateVars(this.templateSrv, query.itServiceFilter, scopedVars),
slaFilter: utils.replaceTemplateVars(this.templateSrv, query.slaFilter, scopedVars),
itemids: utils.replaceTemplateVars(
this.templateSrv,
query.itemids,
scopedVars,
utils.zabbixItemIdsTemplateFormat
),
textFilter: utils.replaceTemplateVars(this.templateSrv, query.textFilter, scopedVars),
functions: utils.replaceVariablesInFuncParams(this.templateSrv, query.functions, scopedVars),
tags: {
...query.tags,
filter: utils.replaceTemplateVars(this.templateSrv, query.tags?.filter, scopedVars),
},
group: {
...query.group,
filter: utils.replaceTemplateVars(this.templateSrv, query.group?.filter, scopedVars),
},
host: {
...query.host,
filter: utils.replaceTemplateVars(this.templateSrv, query.host?.filter, scopedVars),
},
application: {
...query.application,
filter: utils.replaceTemplateVars(this.templateSrv, query.application?.filter, scopedVars),
},
proxy: {
...query.proxy,
filter: utils.replaceTemplateVars(this.templateSrv, query.proxy?.filter, scopedVars),
},
trigger: {
...query.trigger,
filter: utils.replaceTemplateVars(this.templateSrv, query.trigger?.filter, scopedVars),
},
itemTag: {
...query.itemTag,
filter: utils.replaceTemplateVars(this.templateSrv, query.itemTag?.filter, scopedVars),
},
item: {
...query.item,
filter: utils.replaceTemplateVars(this.templateSrv, query.item?.filter, scopedVars),
},
}; };
}
/**
* Custom formatter for template variables.
* Default Grafana "regex" formatter returns
* value1|value2
* This formatter returns
* (value1|value2)
* This format needed for using in complex regex with
* template variables, for example
* /CPU $cpu_item.*time/ where $cpu_item is system,user,iowait
*/
export function zabbixTemplateFormat(value) {
if (typeof value === 'string') {
return utils.escapeRegex(value);
}
const escapedValues = _.map(value, utils.escapeRegex);
return '(' + escapedValues.join('|') + ')';
}
function zabbixItemIdsTemplateFormat(value) {
if (typeof value === 'string') {
return value;
}
return value.join(',');
}
/**
* If template variables are used in request, replace it using regex format
* and wrap with '/' for proper multi-value work. Example:
* $variable selected as a, b, c
* We use filter $variable
* $variable -> a|b|c -> /a|b|c/
* /$variable/ -> /a|b|c/ -> /a|b|c/
*/
export function replaceTemplateVars(templateSrv, target, scopedVars) {
let replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
if (target && target !== replacedTarget && !utils.isRegex(replacedTarget)) {
replacedTarget = '/^' + replacedTarget + '$/';
}
return replacedTarget;
}
export function base64StringToArrowTable(text: string) {
const b64 = atob(text);
const arr = Uint8Array.from(b64, (c) => {
return c.charCodeAt(0);
}); });
return arr;
}
function getRequestTarget(request: DataQueryRequest<any>, refId: string): any {
for (let i = 0; i < request.targets.length; i++) {
const target = request.targets[i];
if (target.refId === refId) {
return target;
} }
} }
return null;
}
const getTriggersOptions = (target: ZabbixMetricsQuery, timeRange) => {
const [timeFrom, timeTo] = timeRange;
const options: any = {
minSeverity: target.options?.minSeverity,
acknowledged: target.options?.acknowledged,
count: target.options?.count,
};
if (target.options?.useTimeRange) {
options.timeFrom = timeFrom;
options.timeTo = timeTo;
}
return options;
};

View File

@@ -0,0 +1,22 @@
import { useEffect, useMemo, useState } from 'react';
import { ScopedVars } from '@grafana/data';
import { ZabbixDatasource } from '../datasource';
import { ZabbixMetricsQuery } from '../types/query';
const EMPTY_SCOPED_VARS: ScopedVars = {};
export const useInterpolatedQuery = (
datasource: ZabbixDatasource,
query: ZabbixMetricsQuery,
scopedVars?: ScopedVars
): ZabbixMetricsQuery => {
const [interpolatedQuery, setInterpolatedQuery] = useState<ZabbixMetricsQuery>(query);
const resolvedScopedVars = useMemo(() => scopedVars ?? EMPTY_SCOPED_VARS, [scopedVars]);
useEffect(() => {
const replacedQuery = datasource.interpolateVariablesInQueries([query], resolvedScopedVars)[0];
setInterpolatedQuery(replacedQuery);
}, [datasource, query, resolvedScopedVars]);
return interpolatedQuery;
};

View File

@@ -1,12 +1,52 @@
import { dateMath } from '@grafana/data'; import { DataQueryResponse, dateMath } from '@grafana/data';
import _ from 'lodash'; import _ from 'lodash';
import { datasourceSrvMock, templateSrvMock } from '../../test-setup/mocks'; import { datasourceSrvMock, templateSrvMock } from '../../test-setup/mocks';
import { replaceTemplateVars, ZabbixDatasource, zabbixTemplateFormat } from '../datasource';
import { VariableQueryTypes } from '../types'; import { VariableQueryTypes } from '../types';
import { ZabbixDatasource } from 'datasource/datasource';
// firstValueFrom removed - tests call frontendQuery directly for text queries
import * as utils from '../utils';
jest.mock( jest.mock(
'@grafana/runtime', '@grafana/runtime',
() => ({ () => {
const actual = jest.requireActual('@grafana/runtime');
// Provide a custom query implementation that resolves backend + frontend + db + annotations
// so tests relying on merged results receive expected data.
if (actual && actual.DataSourceWithBackend && actual.DataSourceWithBackend.prototype) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
actual.DataSourceWithBackend.prototype.query = function (request: any) {
const that: any = this;
const { from } = require('rxjs');
const backendResponse = Promise.resolve({ data: [] });
const dbPromise = that.dbConnectionQuery ? that.dbConnectionQuery(request) : Promise.resolve({ data: [] });
const fePromise = that.frontendQuery ? that.frontendQuery(request) : Promise.resolve({ data: [] });
const annPromise = that.annotationRequest ? that.annotationRequest(request) : Promise.resolve({ data: [] });
return from(
Promise.all([backendResponse, dbPromise, fePromise, annPromise]).then(([backend, db, fe, ann]) => {
const data: any[] = [];
if (backend && backend.data) {
data.push(...backend.data);
}
if (db && db.data) {
data.push(...db.data);
}
if (fe && fe.data) {
data.push(...fe.data);
}
if (ann && ann.data) {
data.push(...ann.data);
}
return { data };
})
);
};
}
return {
...actual,
getBackendSrv: () => ({ getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({ data: { result: '' } }), datasourceRequest: jest.fn().mockResolvedValue({ data: { result: '' } }),
fetch: () => ({ fetch: () => ({
@@ -20,7 +60,8 @@ jest.mock(
replace: jest.fn().mockImplementation((query) => query), replace: jest.fn().mockImplementation((query) => query),
}), }),
reportInteraction: jest.fn(), reportInteraction: jest.fn(),
}), };
},
{ virtual: true } { virtual: true }
); );
@@ -28,6 +69,24 @@ jest.mock('../components/AnnotationQueryEditor', () => ({
AnnotationQueryEditor: () => {}, AnnotationQueryEditor: () => {},
})); }));
jest.mock(
'../utils',
() => (
jest.requireActual('../utils'),
{
replaceVariablesInFuncParams: jest.fn(),
parseInterval: jest.fn(),
replaceTemplateVars: jest.fn().mockImplementation((templateSrv, prop) => prop),
getRangeScopedVars: jest.fn(),
bindFunctionDefs: jest.fn().mockResolvedValue([]),
parseLegacyVariableQuery: jest.fn(),
formatMetric: jest.fn().mockImplementation((metric) => {
return { text: metric.name, expandable: false };
}),
}
)
);
describe('ZabbixDatasource', () => { describe('ZabbixDatasource', () => {
let ctx: any = {}; let ctx: any = {};
let consoleSpy: jest.SpyInstance; let consoleSpy: jest.SpyInstance;
@@ -101,7 +160,7 @@ describe('ZabbixDatasource', () => {
item: { filter: 'System information' }, item: { filter: 'System information' },
textFilter: '', textFilter: '',
useCaptureGroups: true, useCaptureGroups: true,
queryType: 2, queryType: '2',
resultFormat: 'table', resultFormat: 'table',
options: { options: {
skipEmptyValues: false, skipEmptyValues: false,
@@ -110,25 +169,18 @@ describe('ZabbixDatasource', () => {
]; ];
}); });
it('should return data in table format', (done) => { it('should return data in table format', async () => {
ctx.ds.query(ctx.options).then((result) => { const result = (await ctx.ds.frontendQuery(ctx.options)) as DataQueryResponse;
expect(result.data.length).toBe(1); expect(result.data.length).toBe(1);
let tableData = result.data[0]; let tableData = result.data[0];
expect(tableData.columns).toEqual([ expect(tableData.columns).toEqual([{ text: 'Host' }, { text: 'Item' }, { text: 'Key' }, { text: 'Last value' }]);
{ text: 'Host' },
{ text: 'Item' },
{ text: 'Key' },
{ text: 'Last value' },
]);
expect(tableData.rows).toEqual([['Zabbix server', 'System information', 'system.uname', 'Linux last']]); expect(tableData.rows).toEqual([['Zabbix server', 'System information', 'system.uname', 'Linux last']]);
done();
});
}); });
it('should extract value if regex with capture group is used', (done) => { it('should extract value if regex with capture group is used', (done) => {
ctx.options.targets[0].textFilter = 'Linux (.*)'; ctx.options.targets[0].textFilter = 'Linux (.*)';
ctx.ds.query(ctx.options).then((result) => { ctx.ds.frontendQuery(ctx.options).then((result) => {
let tableData = result.data[0]; let tableData = result.data[0];
expect(tableData.rows[0][3]).toEqual('last'); expect(tableData.rows[0][3]).toEqual('last');
done(); done();
@@ -163,7 +215,7 @@ describe('ZabbixDatasource', () => {
{ clock: '1500010500', itemid: '90109', ns: '900111000', value: '' }, { clock: '1500010500', itemid: '90109', ns: '900111000', value: '' },
]) ])
); );
return ctx.ds.query(ctx.options).then((result) => { return ctx.ds.frontendQuery(ctx.options).then((result) => {
let tableData = result.data[0]; let tableData = result.data[0];
expect(tableData.rows.length).toBe(1); expect(tableData.rows.length).toBe(1);
expect(tableData.rows[0][3]).toEqual('Linux last'); expect(tableData.rows[0][3]).toEqual('Linux last');
@@ -171,69 +223,34 @@ describe('ZabbixDatasource', () => {
}); });
}); });
describe('When replacing template variables', () => {
function testReplacingVariable(target, varValue, expectedResult, done) {
ctx.ds.replaceTemplateVars = _.partial(replaceTemplateVars, {
replace: jest.fn((target) => zabbixTemplateFormat(varValue)),
});
let result = ctx.ds.replaceTemplateVars(target);
expect(result).toBe(expectedResult);
done();
}
/*
* Alphanumerics, spaces, dots, dashes and underscores
* are allowed in Zabbix host name.
* 'AaBbCc0123 .-_'
*/
it('should return properly escaped regex', (done) => {
let target = '$host';
let template_var_value = 'AaBbCc0123 .-_';
let expected_result = '/^AaBbCc0123 \\.-_$/';
testReplacingVariable(target, template_var_value, expected_result, done);
});
/*
* Single-value variable
* $host = backend01
* $host => /^backend01|backend01$/
*/
it('should return proper regex for single value', (done) => {
let target = '$host';
let template_var_value = 'backend01';
let expected_result = '/^backend01$/';
testReplacingVariable(target, template_var_value, expected_result, done);
});
/*
* Multi-value variable
* $host = [backend01, backend02]
* $host => /^(backend01|backend01)$/
*/
it('should return proper regex for multi-value', (done) => {
let target = '$host';
let template_var_value = ['backend01', 'backend02'];
let expected_result = '/^(backend01|backend02)$/';
testReplacingVariable(target, template_var_value, expected_result, done);
});
});
describe('When invoking metricFindQuery() with legacy query', () => { describe('When invoking metricFindQuery() with legacy query', () => {
beforeEach(() => { beforeEach(() => {
ctx.ds.replaceTemplateVars = (str) => str;
ctx.ds.zabbix = { ctx.ds.zabbix = {
getGroups: jest.fn().mockReturnValue(Promise.resolve([])), getGroups: jest.fn().mockReturnValue(Promise.resolve([])),
getHosts: jest.fn().mockReturnValue(Promise.resolve([])), getHosts: jest.fn().mockReturnValue(Promise.resolve([])),
getApps: jest.fn().mockReturnValue(Promise.resolve([])), getApps: jest.fn().mockReturnValue(Promise.resolve([])),
getItems: jest.fn().mockReturnValue(Promise.resolve([])), getItems: jest.fn().mockReturnValue(Promise.resolve([])),
}; };
jest.spyOn(utils, 'replaceTemplateVars').mockImplementation(({}, prop: string, {}) => {
return prop;
});
}); });
it('should return groups', (done) => { it('should return groups', (done) => {
jest.spyOn(utils, 'parseLegacyVariableQuery').mockImplementation((query: string) => {
let group = '';
if (query === '*') {
group = '/.*/';
} else {
group = query;
}
return {
queryType: VariableQueryTypes.Group,
group: group,
};
});
const tests = [ const tests = [
{ query: '*', expect: '/.*/' }, { query: '*', expect: '/.*/' },
{ query: 'Backend', expect: 'Backend' }, { query: 'Backend', expect: 'Backend' },
@@ -259,6 +276,14 @@ describe('ZabbixDatasource', () => {
}); });
it('should return hosts', (done) => { it('should return hosts', (done) => {
jest.spyOn(utils, 'parseLegacyVariableQuery').mockImplementation((query: string) => {
let splits = query.split('.');
return {
queryType: VariableQueryTypes.Host,
group: splits[0] === '*' ? '/.*/' : splits[0],
host: splits[1] === '*' ? '/.*/' : splits[1],
};
});
const tests = [ const tests = [
{ query: '*.*', expect: ['/.*/', '/.*/'] }, { query: '*.*', expect: ['/.*/', '/.*/'] },
{ query: '.', expect: ['', ''] }, { query: '.', expect: ['', ''] },
@@ -275,6 +300,15 @@ describe('ZabbixDatasource', () => {
}); });
it('should return applications', (done) => { it('should return applications', (done) => {
jest.spyOn(utils, 'parseLegacyVariableQuery').mockImplementation((query: string) => {
let splits = query.split('.');
return {
queryType: VariableQueryTypes.Application,
group: splits[0] === '*' ? '/.*/' : splits[0],
host: splits[1] === '*' ? '/.*/' : splits[1],
application: splits[2] === '*' ? '/.*/' : splits[2],
};
});
const tests = [ const tests = [
{ query: '*.*.*', expect: ['/.*/', '/.*/', '/.*/'] }, { query: '*.*.*', expect: ['/.*/', '/.*/', '/.*/'] },
{ query: '.*.', expect: ['', '/.*/', ''] }, { query: '.*.', expect: ['', '/.*/', ''] },
@@ -291,6 +325,16 @@ describe('ZabbixDatasource', () => {
}); });
it('should return items', (done) => { it('should return items', (done) => {
jest.spyOn(utils, 'parseLegacyVariableQuery').mockImplementation((query: string) => {
let splits = query.split('.');
return {
queryType: VariableQueryTypes.Item,
group: splits[0] === '*' ? '/.*/' : splits[0],
host: splits[1] === '*' ? '/.*/' : splits[1],
application: splits[2] === '*' ? '' : splits[2],
item: splits[3] === '*' ? '/.*/' : splits[3],
};
});
const tests = [ const tests = [
{ query: '*.*.*.*', expect: ['/.*/', '/.*/', '', undefined, '/.*/', { showDisabledItems: undefined }] }, { query: '*.*.*.*', expect: ['/.*/', '/.*/', '', undefined, '/.*/', { showDisabledItems: undefined }] },
{ query: '.*.*.*', expect: ['', '/.*/', '', undefined, '/.*/', { showDisabledItems: undefined }] }, { query: '.*.*.*', expect: ['', '/.*/', '', undefined, '/.*/', { showDisabledItems: undefined }] },
@@ -320,6 +364,14 @@ describe('ZabbixDatasource', () => {
}); });
it('should invoke method with proper arguments', (done) => { it('should invoke method with proper arguments', (done) => {
jest.spyOn(utils, 'parseLegacyVariableQuery').mockImplementation((query: string) => {
let splits = query.split('.');
return {
queryType: VariableQueryTypes.Host,
group: splits[0] === '*' ? '/.*/' : splits[0],
host: splits[1] === '*' ? '/.*/' : splits[1],
};
});
let query = '*.*'; let query = '*.*';
ctx.ds.metricFindQuery(query); ctx.ds.metricFindQuery(query);
@@ -329,7 +381,6 @@ describe('ZabbixDatasource', () => {
describe('When invoking metricFindQuery()', () => { describe('When invoking metricFindQuery()', () => {
beforeEach(() => { beforeEach(() => {
ctx.ds.replaceTemplateVars = (str) => str;
ctx.ds.zabbix = { ctx.ds.zabbix = {
getGroups: jest.fn().mockReturnValue(Promise.resolve([{ name: 'Group1' }, { name: 'Group2' }])), getGroups: jest.fn().mockReturnValue(Promise.resolve([{ name: 'Group1' }, { name: 'Group2' }])),
getHosts: jest.fn().mockReturnValue(Promise.resolve([{ name: 'Host1' }, { name: 'Host2' }])), getHosts: jest.fn().mockReturnValue(Promise.resolve([{ name: 'Host1' }, { name: 'Host2' }])),

View File

@@ -1,5 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../utils'; import * as utils from '../utils';
import { replaceTemplateVars, zabbixTemplateFormat } from '../utils';
describe('Utils', () => { describe('Utils', () => {
describe('expandItemName()', () => { describe('expandItemName()', () => {
@@ -174,4 +175,90 @@ describe('Utils', () => {
} }
}); });
}); });
describe('replaceTemplateVars()', () => {
function testReplacingVariable(target, varValue, expectedResult, done) {
const templateSrv = {
replace: jest.fn((target) => zabbixTemplateFormat(varValue)),
getVariables: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
let result = replaceTemplateVars(templateSrv, target, {});
expect(result).toBe(expectedResult);
done();
}
/*
* Alphanumerics, spaces, dots, dashes and underscores
* are allowed in Zabbix host name.
* 'AaBbCc0123 .-_'
*/
it('should return properly escaped regex', (done) => {
let target = '$host';
let template_var_value = 'AaBbCc0123 .-_';
let expected_result = '/^AaBbCc0123 \\.-_$/';
testReplacingVariable(target, template_var_value, expected_result, done);
});
/*
* Single-value variable
* $host = backend01
* $host => /^backend01|backend01$/
*/
it('should return proper regex for single value', (done) => {
let target = '$host';
let template_var_value = 'backend01';
let expected_result = '/^backend01$/';
testReplacingVariable(target, template_var_value, expected_result, done);
});
/*
* Multi-value variable
* $host = [backend01, backend02]
* $host => /^(backend01|backend01)$/
*/
it('should return proper regex for multi-value', (done) => {
let target = '$host';
let template_var_value = ['backend01', 'backend02'];
let expected_result = '/^(backend01|backend02)$/';
testReplacingVariable(target, template_var_value, expected_result, done);
});
});
describe('replaceVariablesInFuncParams()', () => {
it('should interpolate numeric and string params with templateSrv', () => {
const replaceMock = jest
.fn()
.mockImplementation((value) => (value === '42' ? '100' : value.replace('$var', 'result')));
const templateSrv = { replace: replaceMock };
const scopedVars = { some: 'var' } as any;
const functions = [
{
def: { name: 'test' },
params: [42, '$var'],
},
];
const [fn] = utils.replaceVariablesInFuncParams(templateSrv as any, functions as any, scopedVars);
expect(replaceMock).toHaveBeenCalledWith('42', scopedVars);
expect(replaceMock).toHaveBeenCalledWith('$var', scopedVars);
expect(fn.params).toEqual([100, 'result']);
});
it('should keep params undefined when function has none', () => {
const templateSrv = { replace: jest.fn() };
const functions = [{ def: { name: 'noop' } }];
const [fn] = utils.replaceVariablesInFuncParams(templateSrv as any, functions as any, {} as any);
expect(fn.params).toBeUndefined();
expect(templateSrv.replace).not.toHaveBeenCalled();
});
});
}); });

View File

@@ -5,13 +5,19 @@ import * as c from './constants';
import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types'; import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types';
import { import {
DataFrame, DataFrame,
DataQueryRequest,
FieldType, FieldType,
getValueFormats, getValueFormats,
MappingType, MappingType,
rangeUtil, rangeUtil,
ScopedVars,
TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_TIME_FIELD_NAME,
ValueMapping, ValueMapping,
} from '@grafana/data'; } from '@grafana/data';
import * as metricFunctions from './metricFunctions';
import dataProcessor from './dataProcessor';
import { MetricFunc, ZabbixMetricsQuery } from './types/query';
import { TemplateSrv } from '@grafana/runtime';
/* /*
* This regex matches 3 types of variable reference with an optional format specifier * This regex matches 3 types of variable reference with an optional format specifier
@@ -200,24 +206,12 @@ function isContainsBraces(query) {
} }
// Pattern for testing regex // Pattern for testing regex
export const regexPattern = /^\/(.*)\/([gmi]*)$/m; const regexPattern = /^\/(.*)\/([gmi]*)$/m;
export function isRegex(str) { export function isRegex(str) {
return regexPattern.test(str); return regexPattern.test(str);
} }
export function isTemplateVariable(str, templateVariables) {
const variablePattern = /^\$\w+/;
if (variablePattern.test(str)) {
const variables = _.map(templateVariables, (variable) => {
return '$' + variable.name;
});
return _.includes(variables, str);
} else {
return false;
}
}
export function getRangeScopedVars(range) { export function getRangeScopedVars(range) {
const msRange = range.to.diff(range.from); const msRange = range.to.diff(range.from);
const sRange = Math.round(msRange / 1000); const sRange = Math.round(msRange / 1000);
@@ -254,7 +248,7 @@ export function parseItemInterval(interval: string): number {
return 0; return 0;
} }
export function normalizeZabbixInterval(interval: string): string { function normalizeZabbixInterval(interval: string): string {
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)?/g; const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)?/g;
const parsedInterval = intervalPattern.exec(interval); const parsedInterval = intervalPattern.exec(interval);
if (!parsedInterval || !interval || (parsedInterval.length > 2 && !parsedInterval[2])) { if (!parsedInterval || !interval || (parsedInterval.length > 2 && !parsedInterval[2])) {
@@ -324,40 +318,6 @@ export function formatAcknowledges(acknowledges) {
} }
} }
export function convertToZabbixAPIUrl(url) {
const zabbixAPIUrlPattern = /.*api_jsonrpc.php$/;
const trimSlashPattern = /(.*?)[\/]*$/;
if (url.match(zabbixAPIUrlPattern)) {
return url;
} else {
return url.replace(trimSlashPattern, '$1');
}
}
/**
* Wrap function to prevent multiple calls
* when waiting for result.
*/
export function callOnce(func, promiseKeeper) {
return function () {
if (!promiseKeeper) {
promiseKeeper = Promise.resolve(
func
.apply(this, arguments)
.then((result) => {
promiseKeeper = null;
return result;
})
.catch((err) => {
promiseKeeper = null;
throw err;
})
);
}
return promiseKeeper;
};
}
/** /**
* Apply function one by one: `sequence([a(), b(), c()]) = c(b(a()))` * Apply function one by one: `sequence([a(), b(), c()]) = c(b(a()))`
* @param {*} funcsArray functions to apply * @param {*} funcsArray functions to apply
@@ -371,24 +331,6 @@ export function sequence(funcsArray) {
}; };
} }
const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/;
export function isValidVersion(version) {
return versionPattern.exec(version);
}
export function parseVersion(version: string) {
const match = versionPattern.exec(version);
if (!match) {
return null;
}
const major = Number(match[1]);
const minor = Number(match[2] || 0);
const patch = Number(match[3] || 0);
const meta = match[4];
return { major, minor, patch, meta };
}
/** /**
* Replaces any space-like symbols (tabs, new lines, spaces) by single whitespace. * Replaces any space-like symbols (tabs, new lines, spaces) by single whitespace.
*/ */
@@ -543,3 +485,125 @@ export function swap<T>(list: T[], n: number, k: number): T[] {
} }
return newList; return newList;
} }
export function bindFunctionDefs(functionDefs, category) {
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
const aggFuncDefs = _.filter(functionDefs, (func) => {
return _.includes(aggregationFunctions, func.def.name) && func.params.length > 0;
});
return _.map(aggFuncDefs, (func) => {
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(dataProcessor.metricFunctions);
});
}
export function getConsolidateBy(target) {
let consolidateBy;
const funcDef = _.find(target.functions, (func) => {
return func.def.name === 'consolidateBy';
});
if (funcDef && funcDef.params && funcDef.params.length) {
consolidateBy = funcDef.params[0];
}
return consolidateBy;
}
export function formatMetric(metricObj) {
return {
text: metricObj.name,
expandable: false,
};
}
/**
* Custom formatter for template variables.
* Default Grafana "regex" formatter returns
* value1|value2
* This formatter returns
* (value1|value2)
* This format needed for using in complex regex with
* template variables, for example
* /CPU $cpu_item.*time/ where $cpu_item is system,user,iowait
*/
export function zabbixTemplateFormat(value) {
if (typeof value === 'string') {
return escapeRegex(value);
}
const escapedValues = _.map(value, escapeRegex);
return '(' + escapedValues.join('|') + ')';
}
export function zabbixItemIdsTemplateFormat(value) {
if (typeof value === 'string') {
return value;
}
return value.join(',');
}
/**
* If template variables are used in request, replace it using regex format
* and wrap with '/' for proper multi-value work. Example:
* $variable selected as a, b, c
* We use filter $variable
* $variable -> a|b|c -> /a|b|c/
* /$variable/ -> /a|b|c/ -> /a|b|c/
*/
export function replaceTemplateVars(
templateSrv: TemplateSrv,
target: string,
scopedVars: ScopedVars,
format: any = zabbixTemplateFormat
) {
let replacedTarget = templateSrv.replace(target, scopedVars, format);
if (target && target !== replacedTarget && !isRegex(replacedTarget)) {
replacedTarget = '/^' + replacedTarget + '$/';
}
return replacedTarget;
}
export function replaceVariablesInFuncParams(
templateSrv: TemplateSrv,
functions: MetricFunc[],
scopedVars: ScopedVars
) {
return functions?.map((func) => {
const interpolatedParams = func?.params?.map((param) => {
if (typeof param === 'number') {
return +templateSrv.replace(param.toString(), scopedVars);
} else {
return templateSrv.replace(param, scopedVars);
}
});
return {
...func,
params: interpolatedParams,
};
});
}
export function getRequestTarget(request: DataQueryRequest<any>, refId: string): any {
for (let i = 0; i < request.targets.length; i++) {
const target = request.targets[i];
if (target.refId === refId) {
return target;
}
}
return null;
}
export 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;
};