Variables: Allow fetching disabled items for Item type variable (#2109)

This PR adds support for showing disabled items when using the `Item`
type template variable. Similar to how we support disabled items today
in our query editor:


<img width="435" height="254" alt="Screenshot 2025-10-21 at 9 00 11 AM"
src="https://github.com/user-attachments/assets/832537c8-84c3-45fe-a85d-b16c8e15f759"
/>

In this example, the host contains a disabled item `CPU iowait time`

<img width="1763" height="46" alt="Screenshot 2025-10-21 at 9 02 08 AM"
src="https://github.com/user-attachments/assets/85419e88-280d-4dce-baee-bf403e1de05d"
/>

Which we can now show and hide from the variable in Grafana:



https://github.com/user-attachments/assets/eca9327e-40a6-4852-92e9-71ff1ad9ea32

I also removed some deprecated types and packages :)!
Fixes: #2025

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jocelyn Collado-Kuri
2025-10-28 19:57:55 -07:00
committed by GitHub
parent 045c708c69
commit 86b7328f39
5 changed files with 107 additions and 40 deletions

View File

@@ -0,0 +1,5 @@
---
'grafana-zabbix': minor
---
Add support for disabled items in Item variable type

View File

@@ -1,12 +1,11 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { parseLegacyVariableQuery } from '../utils'; import { parseLegacyVariableQuery } from '../utils';
import { SelectableValue } from '@grafana/data';
import { VariableQuery, VariableQueryData, VariableQueryProps, VariableQueryTypes } from '../types'; import { VariableQuery, VariableQueryData, VariableQueryProps, VariableQueryTypes } from '../types';
import { ZabbixInput } from './ZabbixInput'; import { ZabbixInput } from './ZabbixInput';
import { InlineField, InlineFieldRow, InlineFormLabel, Input, Select } from '@grafana/ui'; import { Combobox, ComboboxOption, InlineField, InlineFieldRow, InlineFormLabel, Input, Switch } from '@grafana/ui';
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> { export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [ queryTypes: Array<ComboboxOption<VariableQueryTypes>> = [
{ value: VariableQueryTypes.Group, label: 'Group' }, { value: VariableQueryTypes.Group, label: 'Group' },
{ value: VariableQueryTypes.Host, label: 'Host' }, { value: VariableQueryTypes.Host, label: 'Host' },
{ value: VariableQueryTypes.Application, label: 'Application' }, { value: VariableQueryTypes.Application, label: 'Application' },
@@ -23,6 +22,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
application: '', application: '',
itemTag: '', itemTag: '',
item: '', item: '',
showDisabledItems: false,
}; };
constructor(props: VariableQueryProps) { constructor(props: VariableQueryProps) {
@@ -69,34 +69,48 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
}; };
handleQueryChange = () => { handleQueryChange = () => {
const { queryType, group, host, application, itemTag, item } = this.state; const { queryType, group, host, application, itemTag, item, showDisabledItems } = this.state;
const queryModel = { queryType, group, host, application, itemTag, item }; const queryModel = { queryType, group, host, application, itemTag, item, showDisabledItems };
this.props.onChange(queryModel, `Zabbix - ${queryType}`); this.props.onChange(queryModel, `Zabbix - ${queryType}`);
}; };
handleQueryTypeChange = (selectedItem: SelectableValue<VariableQueryTypes>) => { handleQueryTypeChange = (selectedItem: ComboboxOption<VariableQueryTypes>) => {
this.setState({ this.setState({
...this.state, ...this.state,
selectedQueryType: selectedItem, selectedQueryType: selectedItem,
queryType: selectedItem.value, queryType: selectedItem.value,
}); });
const { group, host, application, itemTag, item } = this.state; const { group, host, application, itemTag, item, showDisabledItems } = this.state;
const queryType = selectedItem.value; const queryType = selectedItem.value;
const queryModel = { queryType, group, host, application, itemTag, item }; const queryModel = { queryType, group, host, application, itemTag, item, showDisabledItems };
this.props.onChange(queryModel, `Zabbix - ${queryType}`);
};
handleShowDisabledItemsChange = (evt: React.FormEvent<HTMLInputElement>) => {
const showDisabledItems = (evt.target as any).checked;
this.setState((prevState: VariableQueryData) => {
return {
...prevState,
showDisabledItems: showDisabledItems,
};
});
const { queryType, group, host, application, itemTag, item } = this.state;
const queryModel = { queryType, group, host, application, itemTag, item, showDisabledItems };
this.props.onChange(queryModel, `Zabbix - ${queryType}`); this.props.onChange(queryModel, `Zabbix - ${queryType}`);
}; };
render() { render() {
const { selectedQueryType, legacyQuery, group, host, application, itemTag, item } = this.state; const { selectedQueryType, legacyQuery, group, host, application, itemTag, item, showDisabledItems } = this.state;
const { datasource } = this.props; const { datasource } = this.props;
const supportsItemTags = datasource?.zabbix?.isZabbix54OrHigherSync() || false; const supportsItemTags = datasource?.zabbix?.isZabbix54OrHigherSync() || false;
return ( return (
<> <>
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Query Type" labelWidth={16}> <InlineField label="Query Type" labelWidth={18}>
<Select <Combobox
width={30} width={30}
value={selectedQueryType} value={selectedQueryType}
options={this.queryTypes} options={this.queryTypes}
@@ -106,7 +120,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
</InlineFieldRow> </InlineFieldRow>
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Group" labelWidth={16}> <InlineField label="Group" labelWidth={18}>
<ZabbixInput <ZabbixInput
width={30} width={30}
value={group} value={group}
@@ -118,7 +132,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
{selectedQueryType.value !== VariableQueryTypes.Group && ( {selectedQueryType.value !== VariableQueryTypes.Group && (
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Host" labelWidth={16}> <InlineField label="Host" labelWidth={18}>
<ZabbixInput <ZabbixInput
width={30} width={30}
value={host} value={host}
@@ -136,7 +150,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
<> <>
{supportsItemTags && ( {supportsItemTags && (
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Item Tag" labelWidth={16}> <InlineField label="Item Tag" labelWidth={18}>
<ZabbixInput <ZabbixInput
width={30} width={30}
value={itemTag} value={itemTag}
@@ -149,7 +163,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
{!supportsItemTags && ( {!supportsItemTags && (
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Application" labelWidth={16}> <InlineField label="Application" labelWidth={18}>
<ZabbixInput <ZabbixInput
width={30} width={30}
value={application} value={application}
@@ -163,7 +177,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
{(selectedQueryType.value === VariableQueryTypes.Item || {(selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) && ( selectedQueryType.value === VariableQueryTypes.ItemValues) && (
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Item" labelWidth={16}> <InlineField label="Item" labelWidth={18}>
<ZabbixInput <ZabbixInput
width={30} width={30}
value={item} value={item}
@@ -184,6 +198,15 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
<Input value={legacyQuery} readOnly={true} /> <Input value={legacyQuery} readOnly={true} />
</> </>
)} )}
{selectedQueryType.value === VariableQueryTypes.Item && (
<>
<InlineFieldRow>
<InlineField label="Show disabled items" labelWidth={18} style={{ alignItems: 'center' }}>
<Switch value={showDisabledItems} onChange={this.handleShowDisabledItemsChange} />
</InlineField>
</InlineFieldRow>
</>
)}
</> </>
); );
} }

View File

@@ -11,7 +11,7 @@ import responseHandler from './responseHandler';
import problemsHandler from './problemsHandler'; import problemsHandler from './problemsHandler';
import { Zabbix } from './zabbix/zabbix'; import { Zabbix } from './zabbix/zabbix';
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPIConnector'; import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPIConnector';
import { ProblemDTO, VariableQueryTypes } from './types'; import { LegacyVariableQuery, ProblemDTO, VariableQuery, VariableQueryTypes } from './types';
import { ZabbixMetricsQuery, ShowProblemTypes } from './types/query'; import { ZabbixMetricsQuery, ShowProblemTypes } from './types/query';
import { ZabbixDSOptions } from './types/config'; import { ZabbixDSOptions } from './types/config';
import { import {
@@ -36,6 +36,7 @@ import {
} 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';
export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> { export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDSOptions> {
name: string; name: string;
@@ -212,14 +213,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
let rsp: any; let rsp: any;
try { try {
rsp = await getBackendSrv() rsp = await lastValueFrom(
.fetch({ getBackendSrv().fetch({
url: '/api/ds/query', url: '/api/ds/query',
method: 'POST', method: 'POST',
data: body, data: body,
requestId, requestId,
}) })
.toPromise(); );
} catch (err) { } catch (err) {
return toDataQueryResponse(err); return toDataQueryResponse(err);
} }
@@ -404,7 +405,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
}, },
}; };
const response: any = await getBackendSrv().fetch<any>(requestOptions).toPromise(); const response: any = await lastValueFrom(getBackendSrv().fetch<any>(requestOptions));
return response.data; return response.data;
} }
@@ -813,12 +814,12 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
/** /**
* Find metrics from templated request. * Find metrics from templated request.
* *
* @param {string} query Query from Templating * @param {LegacyVariableQuery} query Query from Templating
* @param options * @param options
* @return {string} Metric name - group, host, app or item or list * @return {string} Metric name - group, host, app or item or list
* of metrics in "{metric1, metric2,..., metricN}" format. * of metrics in "{metric1, metric2,..., metricN}" format.
*/ */
metricFindQuery(query, options) { metricFindQuery(query: LegacyVariableQuery, options) {
let resultPromise; let resultPromise;
let queryModel = _.cloneDeep(query); let queryModel = _.cloneDeep(query);
@@ -835,6 +836,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {}); queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {});
} }
queryModel = queryModel as VariableQuery;
const { group, host, application, item } = queryModel; const { group, host, application, item } = queryModel;
switch (queryModel.queryType) { switch (queryModel.queryType) {
@@ -856,7 +858,8 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
queryModel.host, queryModel.host,
queryModel.application, queryModel.application,
queryModel.itemTag, queryModel.itemTag,
queryModel.item queryModel.item,
{ showDisabledItems: queryModel.showDisabledItems }
); );
break; break;
case VariableQueryTypes.ItemValues: case VariableQueryTypes.ItemValues:

View File

@@ -56,8 +56,8 @@ describe('ZabbixDatasource', () => {
}, },
], ],
range: { range: {
from: dateMath.parse('now-1h'), from: dateMath.toDateTime('now-1h', {}),
to: dateMath.parse('now'), to: dateMath.toDateTime('now', {}),
}, },
}; };
@@ -242,7 +242,7 @@ describe('ZabbixDatasource', () => {
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(ctx.ds.zabbix.getGroups).toBeCalledWith(test.expect); expect(ctx.ds.zabbix.getGroups).toHaveBeenCalledWith(test.expect);
ctx.ds.zabbix.getGroups.mockClear(); ctx.ds.zabbix.getGroups.mockClear();
} }
done(); done();
@@ -250,7 +250,7 @@ describe('ZabbixDatasource', () => {
it('should return empty list for empty query', (done) => { it('should return empty list for empty query', (done) => {
ctx.ds.metricFindQuery('').then((result) => { ctx.ds.metricFindQuery('').then((result) => {
expect(ctx.ds.zabbix.getGroups).toBeCalledTimes(0); expect(ctx.ds.zabbix.getGroups).toHaveBeenCalledTimes(0);
ctx.ds.zabbix.getGroups.mockClear(); ctx.ds.zabbix.getGroups.mockClear();
expect(result).toEqual([]); expect(result).toEqual([]);
@@ -268,7 +268,7 @@ describe('ZabbixDatasource', () => {
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(ctx.ds.zabbix.getHosts).toBeCalledWith(test.expect[0], test.expect[1]); expect(ctx.ds.zabbix.getHosts).toHaveBeenCalledWith(test.expect[0], test.expect[1]);
ctx.ds.zabbix.getHosts.mockClear(); ctx.ds.zabbix.getHosts.mockClear();
} }
done(); done();
@@ -284,7 +284,7 @@ describe('ZabbixDatasource', () => {
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(ctx.ds.zabbix.getApps).toBeCalledWith(test.expect[0], test.expect[1], test.expect[2]); expect(ctx.ds.zabbix.getApps).toHaveBeenCalledWith(test.expect[0], test.expect[1], test.expect[2]);
ctx.ds.zabbix.getApps.mockClear(); ctx.ds.zabbix.getApps.mockClear();
} }
done(); done();
@@ -292,20 +292,27 @@ describe('ZabbixDatasource', () => {
it('should return items', (done) => { it('should return items', (done) => {
const tests = [ const tests = [
{ query: '*.*.*.*', expect: ['/.*/', '/.*/', '', undefined, '/.*/'] }, { query: '*.*.*.*', expect: ['/.*/', '/.*/', '', undefined, '/.*/', { showDisabledItems: undefined }] },
{ query: '.*.*.*', expect: ['', '/.*/', '', undefined, '/.*/'] }, { query: '.*.*.*', expect: ['', '/.*/', '', undefined, '/.*/', { showDisabledItems: undefined }] },
{ query: 'Backend.backend01.*.*', expect: ['Backend', 'backend01', '', undefined, '/.*/'] }, {
{ query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu', undefined, '/.*/'] }, query: 'Backend.backend01.*.*',
expect: ['Backend', 'backend01', '', undefined, '/.*/', { showDisabledItems: undefined }],
},
{
query: 'Back*.*.cpu.*',
expect: ['Back*', '/.*/', 'cpu', undefined, '/.*/', { showDisabledItems: undefined }],
},
]; ];
for (const test of tests) { for (const test of tests) {
ctx.ds.metricFindQuery(test.query); ctx.ds.metricFindQuery(test.query);
expect(ctx.ds.zabbix.getItems).toBeCalledWith( expect(ctx.ds.zabbix.getItems).toHaveBeenCalledWith(
test.expect[0], test.expect[0],
test.expect[1], test.expect[1],
test.expect[2], test.expect[2],
test.expect[3], test.expect[3],
test.expect[4] test.expect[4],
test.expect[5]
); );
ctx.ds.zabbix.getItems.mockClear(); ctx.ds.zabbix.getItems.mockClear();
} }
@@ -316,7 +323,7 @@ describe('ZabbixDatasource', () => {
let query = '*.*'; let query = '*.*';
ctx.ds.metricFindQuery(query); ctx.ds.metricFindQuery(query);
expect(ctx.ds.zabbix.getHosts).toBeCalledWith('/.*/', '/.*/'); expect(ctx.ds.zabbix.getHosts).toHaveBeenCalledWith('/.*/', '/.*/');
done(); done();
}); });
@@ -383,7 +390,33 @@ describe('ZabbixDatasource', () => {
'HostFilter', 'HostFilter',
'AppFilter', 'AppFilter',
'TagFilter', 'TagFilter',
'ItemFilter' 'ItemFilter',
{ showDisabledItems: undefined }
);
expect(result).toEqual([
{ text: 'Item1', expandable: false },
{ text: 'Item2', expandable: false },
]);
});
it('should return disabled items when queryType is Item and show disabled items is turned on', async () => {
const query = {
queryType: VariableQueryTypes.Item,
group: 'GroupFilter',
host: 'HostFilter',
application: 'AppFilter',
itemTag: 'TagFilter',
item: 'ItemFilter',
showDisabledItems: true,
};
const result = await ctx.ds.metricFindQuery(query, {});
expect(ctx.ds.zabbix.getItems).toHaveBeenCalledWith(
'GroupFilter',
'HostFilter',
'AppFilter',
'TagFilter',
'ItemFilter',
{ showDisabledItems: true }
); );
expect(result).toEqual([ expect(result).toEqual([
{ text: 'Item1', expandable: false }, { text: 'Item1', expandable: false },

View File

@@ -1,4 +1,6 @@
import { BusEventWithPayload, DataSourceRef, SelectableValue } from '@grafana/data'; import { BusEventWithPayload } from '@grafana/data';
import { DataSourceRef } from '@grafana/schema';
import { ComboboxOption } from '@grafana/ui';
// The paths of these files have moved around in Grafana and they don't resolve properly // The paths of these files have moved around in Grafana and they don't resolve properly
// either. Safer not to bother trying to import them just for type hinting. // either. Safer not to bother trying to import them just for type hinting.
@@ -60,7 +62,7 @@ export interface VariableQueryProps {
} }
export interface VariableQueryData extends VariableQuery { export interface VariableQueryData extends VariableQuery {
selectedQueryType: SelectableValue<VariableQueryTypes>; selectedQueryType: ComboboxOption<VariableQueryTypes>;
legacyQuery?: string; legacyQuery?: string;
} }
@@ -72,6 +74,7 @@ export interface VariableQuery {
itemTag?: string; itemTag?: string;
item?: string; item?: string;
macro?: string; macro?: string;
showDisabledItems?: boolean;
} }
export type LegacyVariableQuery = VariableQuery | string; export type LegacyVariableQuery = VariableQuery | string;