Add item count warning in query editor for large result sets (#2152)

## Summary

Adds a non-intrusive warning banner in the query editor that alerts
users when their query matches a large number of items (>= 500). This
helps users understand that their query may return a large amount of
data and suggests using more specific filters.

Part of https://github.com/grafana/oss-big-tent-squad/issues/127

## Changes

- Added `ITEM_COUNT_WARNING_THRESHOLD` constant (500 items) in
`src/datasource/constants.ts`
- Created new `ItemCountWarning` component in
`src/datasource/components/ItemCountWarning.tsx`
- Updated `MetricsQueryEditor` to track and report the count of items
matching the current filter
- Integrated the warning component into the main `QueryEditor` component

## How it works

- When items are loaded for the dropdown in the Metrics query editor,
the component counts how many items match the current item filter
- If using a regex filter like `/.*/`, it applies the regex to count
matching items
- If the count is >= 500, a warning banner appears at the top of the
query editor
- The warning is purely informational - queries still execute normally
- The warning only appears for the "Metrics" query type

## Screenshot

The warning appears as a subtle banner with a warning icon:

> I set the limit as 5 just to show the warning

<img width="901" height="298" alt="grafik"
src="https://github.com/user-attachments/assets/a9be8563-1b90-4581-ad15-4e7035b4166e"
/>

## Why

Queries that match thousands of items via wildcard filters (e.g.,
`/.*/`) can return massive amounts of data and potentially overload the
Zabbix server. This proactive warning helps users make informed
decisions about their query scope without adding friction to the normal
query flow.
This commit is contained in:
ismail simsek
2025-12-29 18:49:13 +01:00
committed by GitHub
parent 4eece4b75e
commit 3e626d3aa5
4 changed files with 65 additions and 6 deletions

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Alert } from '@grafana/ui';
import { ITEM_COUNT_WARNING_THRESHOLD } from '../constants';
interface ItemCountWarningProps {
itemCount: number;
threshold?: number;
}
/**
* A warning banner that displays when the query matches too many items.
* This is a non-intrusive warning that doesn't block the query flow.
*/
export const ItemCountWarning: React.FC<ItemCountWarningProps> = ({
itemCount,
threshold = ITEM_COUNT_WARNING_THRESHOLD,
}) => {
if (itemCount < threshold) {
return null;
}
return (
<Alert title="Large number of items" severity="warning">
This query matches {itemCount} items and may return a large amount of data. Consider using more specific filters.
</Alert>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
import { Combobox, ComboboxOption, InlineField, Stack } from '@grafana/ui'; import { Combobox, ComboboxOption, InlineField, Stack } from '@grafana/ui';
import * as c from '../constants'; import * as c from '../constants';
@@ -16,6 +16,7 @@ import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor'; import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor'; import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor';
import { QueryEditorRow } from './QueryEditor/QueryEditorRow'; import { QueryEditorRow } from './QueryEditor/QueryEditorRow';
import { ItemCountWarning } from './ItemCountWarning';
const zabbixQueryTypeOptions: Array<ComboboxOption<QueryType>> = [ const zabbixQueryTypeOptions: Array<ComboboxOption<QueryType>> = [
{ {
@@ -113,6 +114,7 @@ export interface ZabbixQueryEditorProps
extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {} extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {}
export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQueryEditorProps) => { export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQueryEditorProps) => {
const [itemCount, setItemCount] = useState(0);
const queryDefaults = getDefaultQuery(); const queryDefaults = getDefaultQuery();
query = { ...queryDefaults, ...query }; query = { ...queryDefaults, ...query };
query.options = { ...queryDefaults.options, ...query.options }; query.options = { ...queryDefaults.options, ...query.options };
@@ -152,7 +154,12 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
const renderMetricsEditor = () => { const renderMetricsEditor = () => {
return ( return (
<> <>
<MetricsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} /> <MetricsQueryEditor
query={query}
datasource={datasource}
onChange={onChangeInternal}
onItemCountChange={setItemCount}
/>
<QueryFunctionsEditor query={query} onChange={onChangeInternal} /> <QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</> </>
); );
@@ -198,6 +205,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
return ( return (
<Stack direction="column"> <Stack direction="column">
{queryType === c.MODE_METRICS && <ItemCountWarning itemCount={itemCount} />}
<QueryEditorRow> <QueryEditorRow>
<InlineField label="Query type" labelWidth={12}> <InlineField label="Query type" labelWidth={12}>
<Combobox<QueryType> <Combobox<QueryType>

View File

@@ -16,9 +16,10 @@ export interface Props {
query: ZabbixMetricsQuery; query: ZabbixMetricsQuery;
datasource: ZabbixDatasource; datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void; onChange: (query: ZabbixMetricsQuery) => void;
onItemCountChange?: (count: number) => void;
} }
export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => { export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountChange }: Props) => {
const interpolatedQuery = useInterpolatedQuery(datasource, query); const interpolatedQuery = useInterpolatedQuery(datasource, query);
const loadGroupOptions = async () => { const loadGroupOptions = async () => {
@@ -94,12 +95,30 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
return options; return options;
}, [interpolatedQuery.group.filter, interpolatedQuery.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, itemFilter: string) => {
const options = { const options = {
itemtype: 'num', itemtype: 'num',
showDisabledItems: query.options.showDisabledItems, showDisabledItems: query.options.showDisabledItems,
}; };
const items = await datasource.zabbix.getAllItems(group, host, app, itemTag, options); const items = await datasource.zabbix.getAllItems(group, host, app, itemTag, options);
// Count items that match the current item filter for the warning
let matchingItemCount = items?.length || 0;
if (itemFilter && items?.length) {
// If there's an item filter, count how many items match it
const filterRegex =
itemFilter.startsWith('/') && itemFilter.endsWith('/') ? new RegExp(itemFilter.slice(1, -1)) : null;
if (filterRegex) {
matchingItemCount = items.filter((item) => filterRegex.test(item.name)).length;
} else if (itemFilter) {
// Exact match or partial match
matchingItemCount = items.filter((item) => item.name === itemFilter || item.name.includes(itemFilter)).length;
}
}
// Report the matching item count
onItemCountChange?.(matchingItemCount);
let itemOptions: Array<ComboboxOption<string>> = items?.map((item) => ({ let itemOptions: Array<ComboboxOption<string>> = items?.map((item) => ({
value: item.name, value: item.name,
label: item.name, label: item.name,
@@ -114,7 +133,8 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
interpolatedQuery.group.filter, interpolatedQuery.group.filter,
interpolatedQuery.host.filter, interpolatedQuery.host.filter,
interpolatedQuery.application.filter, interpolatedQuery.application.filter,
interpolatedQuery.itemTag.filter interpolatedQuery.itemTag.filter,
interpolatedQuery.item.filter
); );
return options; return options;
}, [ }, [
@@ -122,6 +142,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
interpolatedQuery.host.filter, interpolatedQuery.host.filter,
interpolatedQuery.application.filter, interpolatedQuery.application.filter,
interpolatedQuery.itemTag.filter, interpolatedQuery.itemTag.filter,
interpolatedQuery.item.filter,
]); ]);
// Update suggestions on every metric change // Update suggestions on every metric change
@@ -129,6 +150,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const hostFilter = interpolatedQuery.host?.filter; const hostFilter = interpolatedQuery.host?.filter;
const appFilter = interpolatedQuery.application?.filter; const appFilter = interpolatedQuery.application?.filter;
const tagFilter = interpolatedQuery.itemTag?.filter; const tagFilter = interpolatedQuery.itemTag?.filter;
const itemFilter = interpolatedQuery.item?.filter;
useEffect(() => { useEffect(() => {
fetchGroups(); fetchGroups();
@@ -148,7 +170,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
useEffect(() => { useEffect(() => {
fetchItems(); fetchItems();
}, [groupFilter, hostFilter, appFilter, tagFilter]); }, [groupFilter, hostFilter, appFilter, tagFilter, itemFilter]);
const onFilterChange = (prop: string) => { const onFilterChange = (prop: string) => {
return (value: string) => { return (value: string) => {

View File

@@ -49,3 +49,5 @@ export const MIN_SLA_INTERVAL = 3600;
export const RANGE_VARIABLE_VALUE = 'range_series'; export const RANGE_VARIABLE_VALUE = 'range_series';
export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001; export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001;
export const ITEM_COUNT_WARNING_THRESHOLD = 500;