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 { 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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user