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:
27
src/datasource/components/ItemCountWarning.tsx
Normal file
27
src/datasource/components/ItemCountWarning.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { QueryEditorProps } from '@grafana/data';
|
||||
import { Combobox, ComboboxOption, InlineField, Stack } from '@grafana/ui';
|
||||
import * as c from '../constants';
|
||||
@@ -16,6 +16,7 @@ import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
|
||||
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
|
||||
import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor';
|
||||
import { QueryEditorRow } from './QueryEditor/QueryEditorRow';
|
||||
import { ItemCountWarning } from './ItemCountWarning';
|
||||
|
||||
const zabbixQueryTypeOptions: Array<ComboboxOption<QueryType>> = [
|
||||
{
|
||||
@@ -113,6 +114,7 @@ export interface ZabbixQueryEditorProps
|
||||
extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {}
|
||||
|
||||
export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQueryEditorProps) => {
|
||||
const [itemCount, setItemCount] = useState(0);
|
||||
const queryDefaults = getDefaultQuery();
|
||||
query = { ...queryDefaults, ...query };
|
||||
query.options = { ...queryDefaults.options, ...query.options };
|
||||
@@ -152,7 +154,12 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
||||
const renderMetricsEditor = () => {
|
||||
return (
|
||||
<>
|
||||
<MetricsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
|
||||
<MetricsQueryEditor
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChangeInternal}
|
||||
onItemCountChange={setItemCount}
|
||||
/>
|
||||
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
|
||||
</>
|
||||
);
|
||||
@@ -198,6 +205,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
{queryType === c.MODE_METRICS && <ItemCountWarning itemCount={itemCount} />}
|
||||
<QueryEditorRow>
|
||||
<InlineField label="Query type" labelWidth={12}>
|
||||
<Combobox<QueryType>
|
||||
|
||||
@@ -16,9 +16,10 @@ export interface Props {
|
||||
query: ZabbixMetricsQuery;
|
||||
datasource: ZabbixDatasource;
|
||||
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 loadGroupOptions = async () => {
|
||||
@@ -94,12 +95,30 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
return options;
|
||||
}, [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 = {
|
||||
itemtype: 'num',
|
||||
showDisabledItems: query.options.showDisabledItems,
|
||||
};
|
||||
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) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
@@ -114,7 +133,8 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
interpolatedQuery.group.filter,
|
||||
interpolatedQuery.host.filter,
|
||||
interpolatedQuery.application.filter,
|
||||
interpolatedQuery.itemTag.filter
|
||||
interpolatedQuery.itemTag.filter,
|
||||
interpolatedQuery.item.filter
|
||||
);
|
||||
return options;
|
||||
}, [
|
||||
@@ -122,6 +142,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
interpolatedQuery.host.filter,
|
||||
interpolatedQuery.application.filter,
|
||||
interpolatedQuery.itemTag.filter,
|
||||
interpolatedQuery.item.filter,
|
||||
]);
|
||||
|
||||
// Update suggestions on every metric change
|
||||
@@ -129,6 +150,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
const hostFilter = interpolatedQuery.host?.filter;
|
||||
const appFilter = interpolatedQuery.application?.filter;
|
||||
const tagFilter = interpolatedQuery.itemTag?.filter;
|
||||
const itemFilter = interpolatedQuery.item?.filter;
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
@@ -148,7 +170,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, [groupFilter, hostFilter, appFilter, tagFilter]);
|
||||
}, [groupFilter, hostFilter, appFilter, tagFilter, itemFilter]);
|
||||
|
||||
const onFilterChange = (prop: string) => {
|
||||
return (value: string) => {
|
||||
|
||||
@@ -49,3 +49,5 @@ export const MIN_SLA_INTERVAL = 3600;
|
||||
export const RANGE_VARIABLE_VALUE = 'range_series';
|
||||
|
||||
export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001;
|
||||
|
||||
export const ITEM_COUNT_WARNING_THRESHOLD = 500;
|
||||
|
||||
Reference in New Issue
Block a user